Skill: delibrately-cmyk/cerberus-agent-foundry (version 0.1.2)

--- name: cerberus-agent-foundry description: "Production-ready multi-agent architecture kit for OpenClaw. Provides isolated per-agent workspaces, control-plane orchestration, structured task lifecycle, checksum-verified mailbox messaging, and auditable event logs. Use when implementing Team Lead + specialist agents with secure boundaries, async collaboration, and operational governance." --- # Cerberus Multi-Agent Control Plane A professional blueprint for building, operating, and governing local multi-agent teams in OpenClaw. It ships a hardened protocol stack for role isolation, task orchestration, asynchronous handoffs, and auditability. ## Quick Start 1. Install blueprint into target path: - `bash scripts/install_blueprint.sh /path/to/your/system` 2. Verify task board: - `python3 /path/to/your/system/scripts/taskctl.py list` 3. Verify mailbox: - send: `python3 /path/to/your/system/scripts/mailboxctl.py send --task-id TSK-000 --sender team-lead --receiver coder --correlation-id CORR-001 --body "ACK protocol"` - ack: `python3 /path/to/your/system/scripts/mailboxctl.py status --message-id MSG-0001 --to ACK --actor coder` ## What This Blueprint Provides - Strict workspace isolation per agent (`agents/*/workspace`) - Unified control plane (`control-plane/tasks`, `control-plane/mailbox`, `control-plane/logs`) - Task state machine tooling (`taskctl.py`) - Mailbox protocol tooling with checksum and GC (`mailboxctl.py`) - Shared memory layer under control plane (`control-plane/shared-memory`) ## Core Rules (must enforce) 1. Never use shared agent workspaces. 2. Use only `control-plane/mailbox` for agent-to-agent messaging. 3. Team Lead is sole mailbox garbage-collection authority. 4. Deploy actions require explicit human approval artifacts. ## References - Read `references/operations-checklist.md` for rollout and audit checks. - Read `assets/blueprint/docs/protocol-v1.md` for protocol details.

path: assets/blueprint/agents/coder/identity/TOOLS_DEV.md
content: allowed:
  - read/write files under agents/coder/workspace/
  - compile/test commands on sandboxed project paths
  - git operations on approved repos
forbidden:
  - writes outside agents/coder/workspace/ (except control-plane task/mailbox updates)
  - deployment commands


path: assets/blueprint/agents/qa-ops/identity/TOOLS_OPS.md
content: allowed:
  - run tests
  - inspect logs
  - produce quality reports
forbidden:
  - deploy/restart/prod mutations without human approval artifact
approval_flow:
  - write request to control-plane/approvals/REQ-*.md
  - wait for human approver token


path: assets/blueprint/agents/researcher/identity/TOOLS_SEARCH.md
content: allowed:
  - web search and reading tools
  - document parsing
forbidden:
  - writing to project files
  - shell command execution
scope:
  - read-only outputs into mailbox reports


path: assets/blueprint/agents/team-lead/identity/ROLE.md
content: role: Team Lead (Cerberus)
responsibilities:
  - decompose goals into tasks
  - assign owners and reviewer
  - enforce state machine
  - resolve conflicts
  - archive mailbox and perform GC
constraints:
  - do not execute coding tasks directly unless emergency override


path: assets/blueprint/control-plane/shared-memory/README.md
content: # Shared Memory (Control Plane)

Purpose: store cross-agent distilled knowledge, not raw per-agent scratchpad.

## Layout
- semantic/: stable, reusable facts/protocols/decisions
- episodic/: key multi-agent event summaries
- index/: retrieval index, tags, pointers
- archive/: archived memory snapshots

## Write policy
- Team Lead: read/write (arbiter)
- Coder/Researcher/QA-Ops: read + propose-write via mailbox message to Team Lead

## Guardrails
- Do not store private agent scratch content here.
- Prefer concise, verifiable entries.


path: assets/blueprint/control-plane/shared-memory/policy.yaml
content: version: "1.0"
ownership:
  write_owner: team-lead
  propose_via_mailbox: true
retention:
  episodic_days: 30
  semantic_days: -1
  archive_days: 180


path: assets/blueprint/control-plane/tasks/TSK-000.bootstrap.yaml
content: task_id: TSK-000
title: "Bootstrap multi-agent local protocol v1"
owner: team-lead
reviewer: qa-ops
status: DONE
deadline: "2026-03-01T06:00:00+08:00"
claim_ttl_minutes: 120
dod:
  - "protocol-v1.md exists"
  - "isolated agent workspaces created"
  - "task/message templates created"
notes:
  - "Initialized by Cerberus"
created_at: "2026-03-01T02:30:00+08:00"
updated_at: "2026-03-01T02:30:00+08:00"


path: assets/blueprint/control-plane/templates/message.template.md
content: ---
message_id: MSG-0001
correlation_id: CORR-0001
task_id: TSK-001
sender: team-lead
receiver: coder
version: "1.0"
timestamp: "2026-03-01T02:30:00+08:00"
status: UNREAD
retry_count: 0
checksum: ""
---

# Message

Context:

Requested action:

Acceptance hint:


path: assets/blueprint/control-plane/templates/task.template.yaml
content: task_id: TSK-001
title: ""
owner: ""
reviewer: ""
status: INBOX
deadline: "2026-03-01T12:00:00+08:00"
claim_ttl_minutes: 120
dod:
  - ""
notes: []
created_at: ""
updated_at: ""


path: assets/blueprint/docs/protocol-v1.md
content: # Local Protocol v1 (Cerberus Team)

## Non-Negotiable Isolation
Each agent has a dedicated workspace and **must not** write to other agents' workspaces.

- `agents/team-lead/workspace/`
- `agents/coder/workspace/`
- `agents/researcher/workspace/`
- `agents/qa-ops/workspace/`

Shared coordination happens only in `control-plane/`.

## Task State Machine
`INBOX -> CLAIMED -> IN_PROGRESS -> REVIEW -> DONE | BLOCKED | FAILED`

### Hard constraints
1. CLAIMED requires `owner`, `claimed_at`, `claim_ttl_minutes`.
2. CLAIMED timeout auto-reverts to INBOX.
3. REVIEW must be done by a reviewer different from owner.
4. Every transition appends one record to `control-plane/logs/events.jsonl`.

## Task Schema (required fields)
- task_id
- title
- owner
- status
- deadline
- dod (Definition of Done)
- reviewer
- created_at
- updated_at
- claim_ttl_minutes

## Mailbox Protocol
Message file format: YAML header + markdown body.

Required header:
- message_id
- correlation_id
- task_id
- sender
- receiver
- version
- timestamp
- status: UNREAD | ACK | RESOLVED | REJECTED
- retry_count
- checksum

### Mailbox constraints
- Sender writes append-only new message files.
- Receiver may only update `status` and add response section.
- Only Team Lead can archive/delete old mail.

## Least Privilege Matrix
- Researcher: search/read only, no write, no shell execution.
- Coder: read/write/exec only inside `agents/coder/workspace`.
- QA/Ops: tests/log inspection; deploy requires human approval by writing request into `control-plane/approvals/`.
- Team Lead: routing, arbitration, GC, protocol enforcement.

## Rollout Gates
- Gate 1 (MVP): Team Lead -> Coder -> Mailbox -> Team Lead
- Gate 2: add QA review loop
- Gate 3: add Researcher parallel flow
- Gate 4: consider A2A bridge only after 3+ days stable autonomous runs


## Shared Memory Layer
Shared memory location: `control-plane/shared-memory/`

- `semantic/`: durable cross-agent facts and decisions
- `episodic/`: summarized multi-agent events
- `index/`: retrieval indices/tags
- `archive/`: archived snapshots

Write policy:
- Team Lead: read/write
- Other agents: read + propose via mailbox message to Team Lead


Mailbox topology rule: use only `control-plane/mailbox/` for agent-to-agent messages. Do not create per-agent inbox/outbox channels.


path: assets/blueprint/scripts/mailboxctl.py
content: #!/usr/bin/env python3
import argparse, hashlib, os, re
from datetime import datetime
import yaml

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAILBOX = os.path.join(ROOT, 'control-plane', 'mailbox')
ARCHIVE = os.path.join(ROOT, 'control-plane', 'mailbox', 'archive')
EVENTS = os.path.join(ROOT, 'control-plane', 'logs', 'events.jsonl')

STATUS_FLOW = {
    'UNREAD': {'ACK', 'REJECTED'},
    'ACK': {'RESOLVED', 'REJECTED'},
    'RESOLVED': set(),
    'REJECTED': set(),
}


def now_iso():
    return datetime.now().astimezone().isoformat()


def ensure_dirs():
    os.makedirs(MAILBOX, exist_ok=True)
    os.makedirs(ARCHIVE, exist_ok=True)


def parse_message(path):
    with open(path, 'r', encoding='utf-8') as f:
        txt = f.read()
    m = re.match(r'^---\n(.*?)\n---\n(.*)$', txt, re.S)
    if not m:
        raise SystemExit(f'invalid message format: {path}')
    header_raw, body = m.group(1), m.group(2)
    header = yaml.safe_load(header_raw) or {}
    return header, body


def render_message(header, body):
    y = yaml.safe_dump(header, sort_keys=False, allow_unicode=True).strip()
    return f"---\n{y}\n---\n{body if body.endswith(chr(10)) else body + chr(10)}"


def checksum_for(header, body):
    h = dict(header)
    h.pop('checksum', None)
    payload = yaml.safe_dump(h, sort_keys=True, allow_unicode=True) + "\n" + body
    return hashlib.sha256(payload.encode('utf-8')).hexdigest()


def event(line):
    import json
    with open(EVENTS, 'a', encoding='utf-8') as f:
        f.write(json.dumps(line, ensure_ascii=False) + "\n")


def next_msg_id():
    existing = [x for x in os.listdir(MAILBOX) if x.startswith('MSG-') and x.endswith('.md')]
    nums = []
    for n in existing:
        try:
            nums.append(int(n.split('-')[1].split('.')[0]))
        except Exception:
            pass
    i = max(nums) + 1 if nums else 1
    return f"MSG-{i:04d}"


def message_path(message_id):
    return os.path.join(MAILBOX, f"{message_id}.md")


def required_header(h):
    required = [
        'message_id','correlation_id','task_id','sender','receiver',
        'version','timestamp','status','retry_count','checksum'
    ]
    missing = [k for k in required if k not in h]
    if missing:
        raise SystemExit(f'missing header fields: {missing}')


def cmd_send(a):
    ensure_dirs()
    mid = a.message_id or next_msg_id()
    path = message_path(mid)
    if os.path.exists(path):
        raise SystemExit(f'message exists: {mid}')
    header = {
        'message_id': mid,
        'correlation_id': a.correlation_id,
        'task_id': a.task_id,
        'sender': a.sender,
        'receiver': a.receiver,
        'version': '1.0',
        'timestamp': now_iso(),
        'status': 'UNREAD',
        'retry_count': int(a.retry_count),
        'checksum': ''
    }
    body = f"# Message\n\n{a.body}\n"
    header['checksum'] = checksum_for(header, body)
    with open(path, 'w', encoding='utf-8') as f:
        f.write(render_message(header, body))
    event({'ts': now_iso(), 'type': 'mail_send', 'message_id': mid, 'task_id': a.task_id, 'sender': a.sender, 'receiver': a.receiver})
    print(path)


def cmd_show(a):
    p = message_path(a.message_id)
    h, b = parse_message(p)
    required_header(h)
    chk = checksum_for(h, b)
    ok = (chk == h.get('checksum'))
    print(yaml.safe_dump({'header': h, 'checksum_ok': ok}, sort_keys=False, allow_unicode=True).strip())


def cmd_status(a):
    p = message_path(a.message_id)
    h, b = parse_message(p)
    required_header(h)
    cur = h.get('status')
    nxt = a.to
    if nxt not in STATUS_FLOW.get(cur, set()):
        raise SystemExit(f'invalid status transition: {cur}->{nxt}')

    actor = a.actor
    # Receiver can ACK/RESOLVED/REJECTED for their message; Team Lead can arbitrate any
    if actor != 'team-lead' and actor != h.get('receiver'):
        raise SystemExit('only receiver or team-lead can update status')

    h['status'] = nxt
    h['timestamp'] = now_iso()
    if a.increment_retry:
        h['retry_count'] = int(h.get('retry_count', 0)) + 1
    if a.note:
        b = b + f"\n## Response ({actor})\n\n{a.note}\n"
    h['checksum'] = checksum_for(h, b)
    with open(p, 'w', encoding='utf-8') as f:
        f.write(render_message(h, b))
    event({'ts': now_iso(), 'type': 'mail_status', 'message_id': h['message_id'], 'task_id': h['task_id'], 'from': cur, 'to': nxt, 'actor': actor})
    print(f"{h['message_id']}: {cur} -> {nxt}")


def cmd_list(_):
    ensure_dirs()
    files = sorted([f for f in os.listdir(MAILBOX) if f.endswith('.md')])
    for fn in files:
        h, _ = parse_message(os.path.join(MAILBOX, fn))
        print(f"{h.get('message_id')}\t{h.get('status')}\t{h.get('sender')}\t{h.get('receiver')}\t{h.get('task_id')}")


def cmd_gc(a):
    ensure_dirs()
    if a.actor != 'team-lead':
        raise SystemExit('only team-lead can run mailbox GC')

    statuses = set([x.strip() for x in a.statuses.split(',') if x.strip()])
    moved = 0
    files = sorted([f for f in os.listdir(MAILBOX) if f.endswith('.md')])
    for fn in files:
        src = os.path.join(MAILBOX, fn)
        h, _ = parse_message(src)
        st = h.get('status')
        if st in statuses:
            dst = os.path.join(ARCHIVE, fn)
            os.replace(src, dst)
            moved += 1
            event({'ts': now_iso(), 'type': 'mail_gc', 'message_id': h.get('message_id'), 'task_id': h.get('task_id'), 'status': st, 'actor': a.actor})
    print(f'moved={moved} statuses={sorted(list(statuses))}')


def main():
    ap = argparse.ArgumentParser()
    sp = ap.add_subparsers(required=True)

    s = sp.add_parser('send')
    s.add_argument('--task-id', required=True)
    s.add_argument('--sender', required=True)
    s.add_argument('--receiver', required=True)
    s.add_argument('--correlation-id', required=True)
    s.add_argument('--body', required=True)
    s.add_argument('--retry-count', default=0)
    s.add_argument('--message-id')
    s.set_defaults(func=cmd_send)

    sh = sp.add_parser('show')
    sh.add_argument('--message-id', required=True)
    sh.set_defaults(func=cmd_show)

    st = sp.add_parser('status')
    st.add_argument('--message-id', required=True)
    st.add_argument('--to', required=True, choices=['ACK','RESOLVED','REJECTED'])
    st.add_argument('--actor', required=True)
    st.add_argument('--note')
    st.add_argument('--increment-retry', action='store_true')
    st.set_defaults(func=cmd_status)

    ls = sp.add_parser('list')
    ls.set_defaults(func=cmd_list)

    gc = sp.add_parser('gc')
    gc.add_argument('--actor', required=True)
    gc.add_argument('--statuses', default='RESOLVED,REJECTED')
    gc.set_defaults(func=cmd_gc)

    a = ap.parse_args()
    a.func(a)


if __name__ == '__main__':
    main()


path: assets/blueprint/scripts/taskctl.py
content: #!/usr/bin/env python3
import argparse, json, os, glob
from datetime import datetime
import yaml

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TASK_DIR = os.path.join(ROOT, 'control-plane', 'tasks')
EVENTS = os.path.join(ROOT, 'control-plane', 'logs', 'events.jsonl')

VALID = {'INBOX','CLAIMED','IN_PROGRESS','REVIEW','DONE','BLOCKED','FAILED'}
TRANSITIONS = {
    'INBOX': {'CLAIMED'},
    'CLAIMED': {'IN_PROGRESS','INBOX'},
    'IN_PROGRESS': {'REVIEW','BLOCKED','FAILED'},
    'REVIEW': {'DONE','BLOCKED','FAILED'},
    'DONE': set(),'BLOCKED': set(),'FAILED': set()
}

def now():
    return datetime.now().astimezone().isoformat()

def load(path):
    with open(path,'r',encoding='utf-8') as f:
        return yaml.safe_load(f)

def save(path,data):
    with open(path,'w',encoding='utf-8') as f:
        yaml.safe_dump(data,f,sort_keys=False,allow_unicode=True)

def event(rec):
    with open(EVENTS,'a',encoding='utf-8') as f:
        f.write(json.dumps(rec,ensure_ascii=False)+"\n")

def find(task_id):
    for p in glob.glob(os.path.join(TASK_DIR,'*.yaml')):
        d = load(p)
        if d.get('task_id') == task_id:
            return p,d
    raise SystemExit(f'task not found: {task_id}')

def cmd_list(_):
    rows=[]
    for p in sorted(glob.glob(os.path.join(TASK_DIR,'*.yaml'))):
        d=load(p)
        rows.append((d.get('task_id'),d.get('status'),d.get('owner'),os.path.basename(p)))
    for r in rows:
        print('\t'.join([str(x) for x in r]))

def cmd_transition(a):
    p,d=find(a.task_id)
    cur=d.get('status')
    nxt=a.to
    if nxt not in VALID:
        raise SystemExit('invalid target status')
    if nxt not in TRANSITIONS.get(cur,set()):
        raise SystemExit(f'invalid transition: {cur}->{nxt}')
    if nxt=='CLAIMED' and not a.actor:
        raise SystemExit('CLAIMED requires --actor owner')
    if cur=='IN_PROGRESS' and nxt=='REVIEW' and (a.actor and a.actor==d.get('owner')):
        raise SystemExit('self-review forbidden')
    d['status']=nxt
    d['updated_at']=now()
    if nxt=='CLAIMED':
        d['owner']=a.actor
        d['claimed_at']=now()
    save(p,d)
    event({'ts':now(),'type':'task_transition','task_id':a.task_id,'from':cur,'to':nxt,'actor':a.actor})
    print(f'{a.task_id}: {cur} -> {nxt}')

def main():
    ap=argparse.ArgumentParser()
    sp=ap.add_subparsers(required=True)
    l=sp.add_parser('list'); l.set_defaults(func=cmd_list)
    t=sp.add_parser('transition')
    t.add_argument('--task-id',required=True)
    t.add_argument('--to',required=True)
    t.add_argument('--actor',required=False)
    t.set_defaults(func=cmd_transition)
    a=ap.parse_args(); a.func(a)

if __name__=='__main__':
    main()


path: references/operations-checklist.md
content: # Operations Checklist

## Deployment checklist

- [ ] Agent workspaces exist and are isolated.
- [ ] `control-plane/tasks` has seed task.
- [ ] `control-plane/mailbox` supports send/ack/resolve.
- [ ] `events.jsonl` records task and mailbox transitions.
- [ ] Least-privilege identity files exist per role.

## Stability gate (before expansion)

Run for at least 3 days without manual intervention:
- task lifecycle transitions valid
- no mailbox duplicate-consume incident
- no unauthorized cross-workspace writes

## Security checks

- Coder write scope constrained to coder workspace.
- QA/Ops deploy actions blocked without approval ticket.
- Team Lead-only mailbox GC enforced.


path: scripts/install_blueprint.sh
content: #!/usr/bin/env bash
set -euo pipefail
TARGET_DIR="${1:-./multi-agent-system}"
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$SKILL_DIR/assets/blueprint"
mkdir -p "$TARGET_DIR"
cp -R "$SRC"/* "$TARGET_DIR"/
chmod +x "$TARGET_DIR"/scripts/taskctl.py "$TARGET_DIR"/scripts/mailboxctl.py
printf "installed:%s\n" "$TARGET_DIR"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)