Skip to content

Protocols and Ledger

Argus speaks the two main agent interoperability protocols in both directions: other agents can start and steer councils, and councils can call external tools and delegate to remote agents. Every one of those calls, in or out, is written to a tamper-evident ledger.

flowchart LR
    subgraph inbound [Argus as a server]
        MCPs["/mcp<br/>MCP Streamable HTTP"]
        A2As["/a2a + agent card<br/>A2A JSON-RPC"]
    end
    subgraph outbound [Argus as a client]
        MCPc["MCP servers<br/>tools as mcp__server__tool"]
        A2Ac["A2A agents"]
    end
    ext([External agents and clients]) --> inbound
    inbound --> R[Council runs]
    R --> outbound
    outbound --> peers([External tools and agents])
    inbound -.-> L[(Hash-chained ledger)]
    outbound -.-> L
Protocol Argus as a server Argus as a client
MCP POST /mcp (Streamable HTTP, JSON-RPC). Tools: start_run, get_run, list_runs, intervene, cancel_run, list_councils. Run deliverables are resources, skills are prompts. Register MCP servers (/api/v1/mcp-servers, HTTP, or stdio for admins when ARGUS_MCP_ALLOW_STDIO=true). Their tools appear to agents as mcp__<server>__<tool>, filtered by tool_allow, gated by risk tags and by the server's trust level.
A2A Agent card at /.well-known/agent-card.json (and /.well-known/agent.json), JSON-RPC at /a2a (message/send, tasks/get, tasks/cancel). Each task is a council run. Register remote agents (/api/v1/remote-agents, protocol: a2a). Councils delegate with the remote_agent tool; requires_approval (default true) gates each delegation.

Inbound calls authenticate with an API key (Authorization: Bearer argus_...). Calls that start, steer or cancel runs need the runs:write scope, like POST /api/v1/runs: A2A message/send and tasks/cancel (JSON-RPC error -32010 otherwise), and the MCP start_run, intervene and cancel_run tools. Read-only keys can still query, and viewers can never write.

The members of a council are A2A agents among themselves as well: each has an agent card with a url of the form argus://runs/{run}/agents/{member} and the in-process transport, and they exchange A2A messages and tasks inside Argus, without HTTP (How members talk to each other). This is separate from Argus's own public A2A endpoint and agent card above.

Internal A2A forms

Every internal message carries its form in metadata.form. Besides the eight communication forms (direct, request, broadcast, group, delegate, topic, board, review), the debate adds six. Each has a text part and a data part {"kind": "data", "data": {"type": "argus.<form>", ...}}:

Form Addressed to A2A shape data
claim the board message/send claim_id, author_id, statement, evidence ([{source, quote?}]), confidence, status
challenge the target tasks/send: opens a task, state input-required challenge_id, claim_id, challenger_id, target_id, objection, demand, round
response the challenger message/send on that task, metadata.inReplyTo the challenge challenge_id, responder_id, stance, argument, evidence, unsupported
resolution the target message/send on that task, in reply to the answer challenge_id, outcome, note
position the council message/send member_id, round, conclusion, stances, alternative
signature the council message/send member_id, action (sign, amend, block), reason, amendment, claim_id

A helper has its own agent card while it lives, and its work is a delegate task with metadata.spawned: true, from submitted to working to completed, failed or canceled. All of these are ledgered as protocol a2a, direction internal, like the other council exchanges (Ledger).

Connecting a client

claude mcp add --transport http argus http://localhost:8000/mcp \
  --header "Authorization: Bearer argus_..."

Any MCP client that supports Streamable HTTP works the same way. Through the web container the URL is http://<host>:8080/mcp.

MCP trust levels

Each registered MCP server has a trust level and an optional require_approval list of tool names.

trust Effect
trusted Tools run without extra approval (servers registered before trust levels existed behave this way)
ask The default for new servers. Every tool call goes through the approval flow
blocked The server is not contacted during runs and its tools are never attached

Tools named in require_approval always need approval, whatever the trust level. POST /api/v1/mcp-servers/{id}/discover runs initialize and tools/list and stores the tools; POST /api/v1/remote-agents/{id}/refresh fetches an A2A agent's card again.

Ledger

Every inbound and outbound MCP and A2A interaction is written to a per-user ledger. So is every outbound web request an agent makes through a built-in tool (web_fetch, web_search, the market and news tools), as protocol http, with the host as the peer. Messages and tasks between the members of a council are recorded as protocol a2a with direction internal (Council in the ledger browser). Every use_skill and skill_read call is recorded as protocol skill, direction internal (Skill in the ledger browser): the peer is <name>@<version> (<name>@unknown for a skill that does not exist), the method is use_skill or skill_read, and the detail holds the member and the file path. Browser use is recorded as protocol browser, direction outbound (Browser in the ledger browser): the peer is the page's host, the method is the action (open, click, snapshot, ... plus session.open and session.close), and the detail holds the member, the session id, the page URL and its title. The browse tool is not also recorded as http. Each entry holds the peer, protocol, direction, method, status, latency, run id and SHA-256 digests of the request and response (not the payloads themselves), and is chained to the previous entry:

flowchart LR
    G["genesis<br/>prev_hash = 000…0"] --> E1
    E1["entry 1<br/>request_sha256, response_sha256,<br/>peer, method, status, run"] -->|"hash₁ = sha256(prev ‖ entry₁)"| E2
    E2["entry 2"] -->|"hash₂ = sha256(hash₁ ‖ entry₂)"| E3
    E3["entry 3"] -->|"hash₃ = sha256(hash₂ ‖ entry₃)"| N["…"]

hash = sha256(prev_hash ‖ canonical(entry)). GET /api/v1/ledger/verify replays the chain from the genesis hash and reports the first sequence number where a stored hash no longer matches, so it detects any edit, deletion or reordering. GET /api/v1/ledger lists entries with filters; the web UI's ledger browser (/connections/ledger) shows them and runs the verification.

Connection housekeeping is ledgered too: MCP discovery (initialize, tools/list, whether from the discover endpoint or when a run attaches the server) and A2A card refreshes, with the run id when they happen during a run. During a run, each outbound call is also announced live as a protocol.call event carrying the entry's ledger_id and hash.

Older entries with protocol acp stay in the ledger, because ACP was removed and deleting them would break the chain. They still verify, and the ledger browser shows them as "ACP (removed)". Migration 0022 deleted saved ACP remote agents on upgrade and logged their names.

Do not prune the ledger

Verification replays the chain from its first entry, so deleting any prefix makes it report a broken chain forever after. ARGUS_RETENTION_LEDGER_DAYS is 0 (never prune) by default; only change it if you archive the ledger elsewhere first.