# Agent registry — agents are personas, not queues. # # Per DESIGN-a2a-agents.md v2.1 §2, §5, §5b (commit 714a9ca7), kanboard task # #134 (A2A-2), sibling of #133's model-registry.yaml. An agent is # (identity, Card, Policy, State): {persona/system prompt, memory bank(s), # tool scope, trust class, preferred tier, current backbone}. # # THE ONE-FIELD-BACKBONE-SWAP MECHANISM: an agent's `backbone:` field is the # ONLY thing here that names a concrete model/runtime. Everything a Card # would otherwise duplicate from the model plane (tier the backbone actually # delivers, cost_class, availability a(t), context window) is NOT stored # statically on the agent — it is resolved at read time by dereferencing # `backbone` into model-registry.yaml (via model_registry.py) or, for # non-metered flat-subscription runtimes model-registry.yaml deliberately # excludes (kb#133: "Claude Code ... belongs in the agent registry"), into # the `runtimes:` section below. See agent_registry.py:effective_card(). # This is what makes "switching a backbone is one field" true rather than # aspirational: edit `backbone: kimi` -> `backbone: local-small` and the # derived tier/cost/a(t) change with it, with nothing else to keep in sync. # # `preferred_tier` is a POLICY field (what this persona asks for), distinct # from the backbone's actual delivered tier (a FACT, resolved above) — they # usually agree but don't have to (e.g. a large-preferring agent temporarily # pinned to a small backbone during a quota outage). # # v2.1: personas + Cards live in git, deployed to runtimes — never # live-edited in volumes (kb#156). Several agents below still point at a # live-volume persona (adolf's SOUL.md) because #156 (git-deploy pipeline) # hasn't landed yet; this registry records that fact, it doesn't fix it. # # Trust class is on every Card (§5) and is what kb#140 (capability routing) # and kb#147 (grant enforcement / vault access) consume — see # routing_consumption: at the bottom. Several Cards below (torgash, # researcher, elizaveta's KB identity) describe TARGET state for agents/ # accounts that don't exist yet; each is flagged `status: target` / # `note:` rather than silently implying they're live. Building them is # explicitly out of kb#134's scope (registry only). # # Read with agent_registry.py (same directory): load_registry(), # get_agent(), effective_card(), trust_rank(). schema_version: 1 # ── trust classes (§5) ────────────────────────────────────────────────── # "human > trusted > sandboxed > untrusted". Numeric rank lets routing/grant # code do `>=` comparisons instead of string-matching an ordered list. trust_classes: human: { rank: 3, note: "vault access yes; not MCP-tool-scoped, IS the reasoning" } trusted: { rank: 2, note: "vault access yes (DECIDED, kb#147); outward actions per ask-first rules" } sandboxed: { rank: 1, note: "no vault, no outward sends; scoped MCP allowlist; KB access project-scoped" } untrusted: { rank: 0, note: "anything ingesting the open web; its OUTPUTS are tainted, not just its access restricted" } # ── runtimes ───────────────────────────────────────────────────────────── # Backbones deliberately OUTSIDE model-registry.yaml's `models:` list. # kb#133 excluded Claude Code explicitly: "a flat-subscription runtime, not # a metered API deployment — it belongs in the agent registry (#134), not # here." This is that home. Same lookup contract as a model-registry entry # (id, tier, cost_class, lifecycle, context_tokens) so agent_registry.py's # backbone resolver can treat the two sources uniformly. runtimes: - id: claude-code-cli role: "Claude Code CLI — flat Anthropic subscription runtime (this session's own kind)" tier: large context_tokens: 200000 # current Sonnet/Opus family context window; re-verify on model upgrades cost_class: subscription lifecycle: always-on # gated by Claude Code's own usage windows, not GPU/quota probes quota_probe: "claude-usage (kanboard/bin/claude-usage); orchestration/quota-gating rules documented in kanboard/CLAUDE.md" metered: false opt_in_required: false # ── agents ─────────────────────────────────────────────────────────────── agents: # ── Adolf — proactive auditor / personal assistant ────────────────────── - id: adolf capabilities: [matrix-chat, task-triage, proactive-monitoring, memory-recall, cron-scheduling, browser] persona: role: "proactive auditor / personal assistant, talks to alvis and elizaveta over Matrix" trivial: false prompt_source: current: "container adolf:/home/node/.openclaw/workspace/SOUL.md — lives in the adolf-state Docker VOLUME, live-editable, zero git history. This is the kb#156 migration-debt state; recorded here per kb#134's brief, not migrated." companion_files: ["AGENTS.md", "IDENTITY.md", "TOOLS.md", "USER.md", "HEARTBEAT.md"] # same workspace, same volume, same debt target: "git-controlled path once kb#156 lands (e.g. openai/personas/adolf/SOUL.md), deployed read-only into the volume" trust_class: trusted preferred_tier: large backbone: kimi # resolves via model-registry.yaml (adolf-llm Kimi-CLI wrapper endpoint). Swap this ONE field to rebackbone Adolf. tool_allowlist: mcp_servers: [hindsight, openclaw-tools, kanboard, marketplace, agap] gateway_tools: [cron, nodes, browser] # openclaw.json gateway.tools.allow, live 2026-07-21 vault_access: true # trusted-only per §5 DECIDED; reaches vw_* via the `agap` MCP server note: > "scoped core tools" per kb#134's brief — this IS openai/openclaw.json's live mcp.servers block, not a narrower aspirational allowlist. Per-tool (not per-server) scoping + LiteLLM virtual-key budgets are kb#147's job; this field is the input #147 consumes. memory: banks: - { id: adolf-alvis, role: private, interlocutor: alvis } - { id: adolf-elizaveta, role: private, interlocutor: elizaveta } - { id: adolf-shared, role: shared, interlocutor: household } current_state: > NOT split yet. A single live bank "adolf" (hindsight MCP, http://hindsight:8888/mcp/adolf/, 269+ facts) serves every interlocutor today with no per-human isolation — the exact defect kb#153 exists to fix (depends on this registry existing first). The three banks above are kb#153's target, not current fact. kb_identity: { username: adolf, user_id: 3 } availability_note: "a(t) inherited from backbone at read time (kimi: quota-gated, ~60msg/5h ~300/wk — see model-registry.yaml)" # ── claude-coder — the Claude Code loop as an ordinary consumer ───────── - id: claude-coder capabilities: [coding, kanboard-dispatch, git, infra-ops, code-review] persona: role: "implementer — pulls complex coding tasks from the fabric (design §2 theorem 5, 'never special')" trivial: false prompt_source: current: "git-native, layered: ~/.claude/CLAUDE.md (global user memory) + kanboard/CLAUDE.md (canonical kb orchestration ruleset) + per-repo CLAUDE.md files (e.g. agap_git/CLAUDE.md)" note: "No single SOUL.md — persona is these CLAUDE.md conventions, already git-controlled. No kb#156 debt for this agent." trust_class: trusted preferred_tier: large backbone: claude-code-cli # resolves via runtimes: above (kb#133 exclusion), NOT model-registry.yaml tool_allowlist: mcp_servers: [kanboard, agap] native_tools: [Bash, Read, Edit, Write, Agent, WebFetch, WebSearch, git] vault_access: true # trusted note: > Widest-scoped agent by design (this session's own tool surface) — gated by ask-first rules on outward/destructive actions rather than MCP allowlisting. Per kb#147, a real virtual-key budget still applies. memory: banks: [] model: "session (ephemeral, per invocation) + repo state (git history, CLAUDE.md files, kanboard task/comment history) — no persistent Hindsight bank" kb_identity: { username: claude, user_id: 2 } availability_note: "a(t) inherited from backbone at read time (claude-code-cli: always-on, gated by claude-usage windows)" # ── torgash — marketplace analyst (sandboxed) ─────────────────────────── - id: torgash capabilities: [price-comparison, marketplace-search, cart-ops, product-recommendations] persona: role: "marketplace analyst — price comparison / shopping across Ozon, Yandex Market, etc." trivial: false prompt_source: current: null note: "NOT yet stood up as a running persona — this Card is target state per design §2's agent table and the A2A design-review plan. Building the runtime is out of kb#134's scope (registry only); flagging as follow-up work." trust_class: sandboxed preferred_tier: small backbone: local-small tool_allowlist: mcp_servers: [marketplace] vault_access: false # sandboxed — hard rule §5, no exceptions outward_sends: false note: "scoped to mcp__marketplace__* tools only; no gitea/ha/zabbix/radicale, no kanboard project outside its own." memory: banks: [{ id: torgash, role: private }] current_state: "bank not yet created — target state, same as the persona itself" kb_identity: username: null note: "no Kanboard account provisioned yet; §5 calls for KB access project-scoped to a dedicated project once created — gap, not in kb#134's scope." availability_note: "a(t) inherited from backbone at read time (local-small: always-on, VRAM-bound)" # ── researcher — autonomous, sandboxed/untrusted-input loop ───────────── - id: researcher capabilities: [web-research, synthesis, low-priority-background-loop] persona: role: "autonomous researcher — low-priority self-submitting loop (design §6, §8)" trivial: false prompt_source: { current: null, note: "not yet built; target-state Card, same caveat as torgash." } trust_class: sandboxed trust_note: > Ingests the open web, so its OUTPUTS are tainted regardless of its own sandboxed access level (the untrusted-INPUT rule, §5). Promotion of tainted output into a trusted agent's memory or into any action requires a gate — initially a task to alvis's inbox. preferred_tier: small backbone: local-small # escalates to large via always-ask policy (§5) for synthesis — never silent retry-on-bigger-model tool_allowlist: mcp_servers: [] # target: a scoped web-search/fetch surface once built native_tools: [WebSearch, WebFetch] vault_access: false outward_sends: false memory: banks: [{ id: researcher, role: private }] current_state: "bank not yet created — target state" kb_identity: username: null note: "needs its own KB project(s) per §5 ('researcher gets its own KB project(s)') — not yet created, gap outside kb#134's scope." availability_note: "a(t) inherited from backbone at read time (local-small: always-on, VRAM-bound)" # ── model-agents — trivial personas (from kb#133) ─────────────────────── # Design §2 table row 1: "LLM endpoint (kimi, gemma3:4b, ...) | persona: # trivial (identity) | memory: none". These are direct-address targets # (router mode target=agent-id) for a caller that wants THIS backbone # specifically, bypassing any persona/tool-scope layer — distinct from # e.g. `adolf`, which happens to use the `kimi` backbone today but adds a # persona, memory, and tool scope on top. Only the two chat-capable # models get an agent entry; bge-m3/tei-reranker are non-chat sidecars in # model-registry.yaml, not addressable A2A targets. - id: kimi-endpoint capabilities: [raw-completion] persona: { role: "trivial identity — the kimi backbone exposed directly, no persona layer", trivial: true, prompt_source: { current: null } } trust_class: untrusted # a bare model endpoint makes no trust decisions itself; the caller's trust class governs what reaches it preferred_tier: large backbone: kimi tool_allowlist: { mcp_servers: [], native_tools: [], vault_access: false } memory: { banks: [], model: "none (trivial persona)" } kb_identity: { username: null } - id: local-small-endpoint capabilities: [raw-completion] persona: { role: "trivial identity — the local-small backbone exposed directly, no persona layer", trivial: true, prompt_source: { current: null } } trust_class: untrusted preferred_tier: small backbone: local-small tool_allowlist: { mcp_servers: [], native_tools: [], vault_access: false } memory: { banks: [], model: "none (trivial persona)" } kb_identity: { username: null } # ── humans — first-class agents (§2, §5b) ─────────────────────────────── # "The human being an agent is not a metaphor": approval gates and # decisions are ordinary tasks submitted to a human's inbox, which IS the # Kanboard column/assignment he already processes. No `backbone` — humans # ARE the reasoning, not a resolvable model. - id: alvis capabilities: [approval, decision, escalation-target] persona: { role: "human — primary user/owner of Agap" } trust_class: human preferred_tier: null backbone: null tool_allowlist: null # n/a — outranks `trusted`, not MCP-scoped memory: banks: - { id: adolf-alvis, role: private } - { id: adolf-shared, role: shared } current_state: "today's single unsplit 'adolf' bank mixes alvis + elizaveta content; split pending kb#153" matrix_id: "@admin:mtx.alogins.net" kb_identity: username: admin user_id: 1 note: "assumed == alvis (sole non-bot app-admin account); the Kanboard user record's email field is unpopulated so this can't be confirmed via API — verify by hand if it's ever ambiguous." inbox: "tasks assigned to Kanboard user 'admin' (id 1) across projects — his approval/escalation inbox (design §2)" availability: "a(t) = waking hours (informal, no fixed function yet); vacation mode sets a(t)=0 and parks his inbox per §5b" - id: elizaveta capabilities: [approval, decision, escalation-target] persona: { role: "human — household member" } trust_class: human preferred_tier: null backbone: null tool_allowlist: null memory: banks: - { id: adolf-elizaveta, role: private } - { id: adolf-shared, role: shared } current_state: "not yet split out of the single 'adolf' bank — kb#153, flagged urgent-ish there since she is on Adolf's Matrix allowlist TODAY with a shared, unpartitioned bank." matrix_id: "@elizaveta:mtx.alogins.net" kb_identity: username: null note: "no Kanboard account provisioned for her yet — her only inbox today is the Matrix DM channel (Adolf's dm.allowFrom), not a KB column. Gap / candidate follow-up task, out of kb#134's scope." inbox: "none in Kanboard yet (see kb_identity note above); Matrix DM is the only channel today" # ── memory bank policy (§5b) ───────────────────────────────────────────── # Hard rules the hindsight-memory plugin's recall/retain hooks must enforce # once kb#153 implements bank selection by interlocutor. Registry mirror of # model-registry.yaml's gpu_residency_policy: the parameters a consumer # reads, not the enforcement logic itself. memory_bank_policy: hard_rules: - "content from one human's conversations must never surface to another human (correctness property, not preference)" - "promotion private -> shared happens only by the owning human's explicit action or an approval task — never automatically" - "recall is interlocutor-scoped: the hindsight-memory plugin selects the bank by interlocutor identity (Matrix sender)" - "sandboxed agents (torgash, researcher) read at most the shared bank, never any private bank" banks: - { id: adolf-alvis, owner: alvis, role: private, status: target, note: "kb#153 not yet done" } - { id: adolf-elizaveta, owner: elizaveta, role: private, status: target, note: "kb#153 not yet done" } - { id: adolf-shared, owner: household, role: shared, status: target, note: "kb#153 not yet done" } - { id: adolf, owner: null, role: legacy, status: live-today, note: "single unsplit bank every interlocutor currently reads/writes; superseded by the three rows above once kb#153 lands" } - { id: torgash, owner: torgash, role: private, status: target, note: "agent not yet built" } - { id: researcher, owner: researcher, role: private, status: target, note: "agent not yet built" } # ── how downstream tasks consume this file ──────────────────────────────── # Documents the contract kb#140 (routing) and kb#147 (grant enforcement) # build against, mirroring model-registry.yaml's `routing:` section. routing_consumption: trust_gate: > kb#147: a task requiring vault access may target only an agent whose trust_classes[...].rank >= trust_classes.trusted.rank (i.e. trusted or human). Sandboxed/untrusted agents' tool_allowlist.vault_access is always false by construction above — kb#147's job is to make that provably true at the MCP/LiteLLM enforcement points, not just on paper. capability_routing: > kb#140: given a task's required capabilities + trust constraint, the router filters agents by capabilities ⊆ agent.capabilities, trust rank >= required, and current a(t) == 1 (resolved via agent_registry.py's effective_card(), which dereferences `backbone` into model-registry.yaml or `runtimes:` above) — no submitter-side hardcoded agent id needed.