Add agent registry: persona + prompt + memory + tool scope + trust (kb#134, A2A-2)

Per DESIGN-a2a-agents.md v2.1 §2, §5, §5b: an agent is a persona, not a
queue. Sibling of kb#133's model-registry.yaml, same load/get/CLI shape.

- agent-registry.yaml: populates adolf (trusted, scoped core tools),
  claude-coder (trusted, implementer), torgash (sandboxed, marketplace-
  only, target state - not yet built), researcher (sandboxed/untrusted-
  input, target state), kimi-endpoint/local-small-endpoint (trivial
  model-agents from #133), and alvis/elizaveta as first-class human
  agents with trust=human, own inboxes, own private memory bank targets
  (adolf-alvis/adolf-elizaveta/adolf-shared - split itself is kb#153).
  Also carries trust_classes with numeric rank, a runtimes: section for
  Claude Code (kb#133 explicitly excluded it from model-registry.yaml),
  and memory_bank_policy documenting §5b's hard rules.

- agent_registry.py: effective_card() is what makes "backbone swap is
  one field" true rather than aspirational - tier/cost_class/lifecycle
  are never stored statically on an agent, they're resolved at read
  time from `backbone` via model-registry.yaml or runtimes:. Also
  trust_rank() and can_reach_vault() (kb#147's vault-access DECIDED
  rule as a checkable function, not just a comment).

Several Cards (torgash, researcher, elizaveta's KB identity, the split
memory banks) describe target state for things not yet built/migrated -
flagged with status/note fields rather than implied live, matching the
task's scope (registry only, no runtime migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014t8Qg9gi7H7HtT8MncoXAB
This commit is contained in:
2026-07-21 12:19:46 +00:00
parent b5aaceb65a
commit b126e7aaed
2 changed files with 502 additions and 0 deletions

295
openai/agent-registry.yaml Normal file
View File

@@ -0,0 +1,295 @@
# 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.

207
openai/agent_registry.py Executable file
View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""agent_registry — reads agent-registry.yaml (kb#134, A2A-2).
Sibling of model_registry.py (kb#133): same load/get/CLI shape, same
philosophy ("registry is data, not logic"). This module's one piece of
real logic is effective_card() — the mechanism that makes "switching a
backbone is one field" true rather than aspirational (see the header
comment in agent-registry.yaml): an agent's static fields (persona, trust
class, tool scope, memory banks, preferred tier) never duplicate what the
CURRENT backbone provides (tier, cost_class, availability a(t), context
window). Those are resolved at read time by dereferencing `backbone` into
model-registry.yaml (via model_registry.get_model) or, for runtimes
model-registry.yaml deliberately excludes (kb#133: Claude Code), into this
file's own `runtimes:` section.
Usage (library):
from agent_registry import load_registry, get_agent, effective_card, trust_rank
reg = load_registry()
adolf = get_agent(reg, "adolf")
card = effective_card(reg, "adolf") # -> persona/trust/tools/memory + resolved tier/cost/a(t)
trust_rank(reg, "torgash") < trust_rank(reg, "adolf") # sandboxed < trusted
Usage (CLI, for manual verification):
./agent_registry.py list
./agent_registry.py get --id adolf
./agent_registry.py effective-card --id adolf
./agent_registry.py trust-rank --id torgash
./agent_registry.py can-reach-vault --id torgash # kb#147 sanity check
"""
import argparse
import json
import os
import sys
import yaml
import model_registry as mr
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_REGISTRY_PATH = os.path.join(HERE, "agent-registry.yaml")
class AgentRegistryError(Exception):
pass
def load_registry(path=None):
"""Load and lightly validate agent-registry.yaml."""
path = path or DEFAULT_REGISTRY_PATH
with open(path) as f:
reg = yaml.safe_load(f)
if not reg or "agents" not in reg:
raise AgentRegistryError(f"{path}: missing top-level 'agents' list")
ids = [a["id"] for a in reg["agents"]]
if len(ids) != len(set(ids)):
raise AgentRegistryError(f"{path}: duplicate agent ids in {ids}")
runtime_ids = [r["id"] for r in reg.get("runtimes", [])]
if len(runtime_ids) != len(set(runtime_ids)):
raise AgentRegistryError(f"{path}: duplicate runtime ids in {runtime_ids}")
return reg
def get_agent(registry, agent_id):
for a in registry["agents"]:
if a["id"] == agent_id:
return a
raise AgentRegistryError(f"unknown agent id: {agent_id!r}")
def get_runtime(registry, runtime_id):
for r in registry.get("runtimes", []):
if r["id"] == runtime_id:
return r
raise AgentRegistryError(f"unknown runtime id: {runtime_id!r}")
# ---------------------------------------------------------------------------
# trust_rank — §5 "human > trusted > sandboxed > untrusted" as a comparable
# integer, for kb#140 (routing) / kb#147 (grant enforcement) to use directly
# instead of re-deriving an ordering from the string.
# ---------------------------------------------------------------------------
def trust_rank(registry, agent_id):
a = get_agent(registry, agent_id)
classes = registry.get("trust_classes", {})
cls = a["trust_class"]
if cls not in classes:
raise AgentRegistryError(f"{agent_id}: unknown trust_class {cls!r} (have: {sorted(classes)})")
return classes[cls]["rank"]
def can_reach_vault(registry, agent_id):
"""kb#147 sanity check: vault access = trusted-or-human only (DECIDED).
Cross-checks the agent's declared tool_allowlist.vault_access flag
against its trust rank, so a registry typo (vault_access: true on a
sandboxed agent) is a raised error, not a silent enforcement gap."""
a = get_agent(registry, agent_id)
classes = registry.get("trust_classes", {})
trusted_rank = classes.get("trusted", {}).get("rank")
declared = ((a.get("tool_allowlist") or {}).get("vault_access")) if a.get("tool_allowlist") else False
rank = trust_rank(registry, agent_id)
if declared and rank < trusted_rank:
raise AgentRegistryError(
f"{agent_id}: tool_allowlist.vault_access=true but trust_class="
f"{a['trust_class']!r} (rank {rank}) < trusted (rank {trusted_rank}) — "
"registry inconsistency, fix before this is load-bearing for kb#147"
)
return bool(declared) and rank >= trusted_rank
# ---------------------------------------------------------------------------
# effective_card — the full A2A Agent Card: static agent fields merged with
# the CURRENT backbone's resolved tier/cost_class/a(t)/context_tokens.
# ---------------------------------------------------------------------------
def _resolve_backbone(registry, backbone_id, model_registry=None):
"""backbone can point into either this file's runtimes: (flat-subscription
runtimes model-registry.yaml deliberately excludes, kb#133) or into
model-registry.yaml's models: (everything else). Try runtimes first —
it's the smaller, local list."""
try:
return "runtime", get_runtime(registry, backbone_id)
except AgentRegistryError:
pass
model_registry = model_registry if model_registry is not None else mr.load_registry()
return "model", mr.get_model(model_registry, backbone_id)
def effective_card(registry, agent_id, model_registry=None):
"""Return the full A2A Agent Card for `agent_id`: its own persona/trust/
tools/memory fields plus tier/cost_class/context_tokens/lifecycle
resolved from whatever `backbone` currently names. Humans and trivial
endpoints with backbone=None get resolved_backbone=None — there's
nothing to dereference."""
a = get_agent(registry, agent_id)
card = dict(a) # shallow copy; don't mutate the loaded registry
backbone_id = a.get("backbone")
if backbone_id is None:
card["resolved_backbone"] = None
return card
source, backbone = _resolve_backbone(registry, backbone_id, model_registry)
card["resolved_backbone"] = {
"source": source, # "runtime" | "model" — which file it came from
"id": backbone_id,
"tier": backbone["tier"],
"cost_class": backbone["cost_class"],
"lifecycle": backbone["lifecycle"],
"context_tokens": backbone.get("context_tokens"),
"metered": backbone.get("metered", False),
}
return card
# ---------------------------------------------------------------------------
# CLI — manual verification only, not part of the library contract.
# ---------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--registry", default=None, help="path to agent-registry.yaml (default: sibling file)")
ap.add_argument("--model-registry", default=None, help="path to model-registry.yaml (default: sibling file)")
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("get")
p.add_argument("--id", required=True)
p = sub.add_parser("effective-card")
p.add_argument("--id", required=True)
p = sub.add_parser("trust-rank")
p.add_argument("--id", required=True)
p = sub.add_parser("can-reach-vault")
p.add_argument("--id", required=True)
sub.add_parser("list")
args = ap.parse_args()
reg = load_registry(args.registry)
model_reg = mr.load_registry(args.model_registry) if args.model_registry else None
try:
if args.cmd == "get":
print(json.dumps(get_agent(reg, args.id), indent=2))
elif args.cmd == "effective-card":
print(json.dumps(effective_card(reg, args.id, model_reg), indent=2))
elif args.cmd == "trust-rank":
print(trust_rank(reg, args.id))
elif args.cmd == "can-reach-vault":
ok = can_reach_vault(reg, args.id)
print(json.dumps({"id": args.id, "can_reach_vault": ok}))
sys.exit(0 if ok else 1)
elif args.cmd == "list":
for a in reg["agents"]:
backbone = a.get("backbone") or "-"
print(f"{a['id']:22} trust={a['trust_class']:9} "
f"tier={str(a.get('preferred_tier')):6} backbone={backbone:16} "
f"role={a['persona']['role']}")
except AgentRegistryError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main()