Context
ADR 0001 made every registry resource discoverable, but an Agent found that way is only usable in the next session: it has to be pulled into a harness first. So an agent working on a task cannot use a better-suited agent from the registry for part of it, which is exactly the moment discovery is most useful. The Agent2Agent protocol (A2A, v1.0.0, Linux Foundation) standardises how one running agent hands a task to another it cannot see inside: an Agent Card describes the agent and its endpoint;SendMessage creates a Task that moves
through TASK_STATE_* states and returns Artifacts; GetTask and
CancelTask follow it. ARD already lists A2A Agent Cards as a resource type
(application/a2a-agent-card+json).
Two kinds of agent matter to Observal users:
- Registry Agents. Installable configurations, not services. They run inside a harness.
- Remote agents. Services other teams already run (triage, data, billing agents built on any framework) that publish A2A Agent Cards.
Decision 1: Delegation is an A2A Task, whatever runs it
Every delegation is represented as an A2A v1.0 Task (JSON serialisation:ROLE_USER/ROLE_AGENT, parts as {"text": ...}, TASK_STATE_* states,
Artifacts for results). Observal’s own bookkeeping lives under
metadata.observal. A local run and a remote call look the same to the
caller, and the local runner can be exposed over the A2A wire later without
changing its data model.
Decision 2: Registry Agents run headless in a throwaway copy of the repository
A delegated registry Agent is installed with project scope into a shared clone (git clone --shared, detached at HEAD, remote removed) of the caller’s
repository, plus the caller’s uncommitted and untracked, non-ignored files,
using the same server install call and file
writer as observal agent pull, minus setup commands, lockfile and
active-agent state. Its harness then runs one prompt non-interactively.
- Which harnesses can do this is a verified runtime fact,
headless_run, in the shared harness registry. The CLI adapter’sheadless_commandbuilds the argv; harnesses without an agent flag get the agent’s instructions inlined. Verified today: Claude Code, Kiro CLI, Cursor CLI, Codex, OpenCode, Copilot CLI, Antigravity, Pi. Not yet: Goose, VS Code Copilot. - The child may edit files; those edits never reach the caller’s tree. The
difference between the tree after the agent’s config was written and the tree
it left is returned as a
changes.patchartifact. The caller decides whether togit applyit. Trees are recorded through a private index file: no commits, refs, hooks or signing. The clone borrows the caller’s objects but has its own refs, config, hooks and stash, so git commands a child with a shell runs (branch, stash, commit, push) stay in the copy. This is not a sandbox: a harness with shell access can still reach any path the user can. - Shell access is refused where the harness can refuse it (Claude Code print
mode denies unapproved tools; Kiro trusts only file tools and the agent’s own
MCP servers; Copilot CLI denies
shell; Cursor and Antigravity run sandboxed). The agent’s own reviewed MCP servers are allowed. - Pi loads project-local
.pifiles only for a trusted project, and trusting also runs the project’s extensions. A Pi child is started with--approveonly when Pi already trusts the caller’s repository (itstrust.json, ordefaultProjectTrust: always); otherwise the delegation fails and says so. - Workspaces live under
~/.observal/delegations/ws/, not the shared temp directory, because harnesses read instruction and skill files from parent directories. - A worker that dies leaves nothing behind for long: the next status check or delegation stops its child (on Linux only after confirming the process still runs in that workspace, so a reused pid is never killed) and removes the workspace. Where the child cannot be proven to be ours, it is left to finish and its workspace is removed by a later sweep.
- The depth, cycle and breadth guards read the chain from the environment the delegation sets, or, when a harness starts MCP servers with only an allowlisted environment (Codex), from the task whose workspace the server runs in.
- Outside a git repository the child gets an empty scratch directory.
Decision 3: Remote A2A agents are governed imported entries
A remote agent is registered by its Agent Card URL (POST /api/v1/ard/imports/a2a). It becomes a discovery_entries row with
source_kind = imported, kind = external, media type
application/a2a-agent-card+json. It has no native table: the entry is the
governed record.
- It starts
pendingand needs a reviewer (.../{identifier}/review), exactly like a native submission, authorized the same way: a team-visible agent by its team’s owners and reviewers, a public one by global reviewers, a private one by admins; team owners and reviewers also see their team’s pending agents in discovery so they can inspect them. Visibility is private, team, or public. Approving requires theobs:artifactDigestthe reviewer inspected; the decision is refused (409) when the card changed in between. - The reviewed card is pinned in the entry (
obs:agentCard,obs:a2aInterface). Clients call the pinned endpoint, never a card fetched at call time. Re-registering a card whose content changed sends it back to review. - Identifier:
urn:air:<card host>:a2a:<name>when the card’s host is a real domain, since that host is the publisher. Otherwise the entry is published under this registry’s domain with its own UUID. - Cards are fetched over https, without following redirects, capped at
256 KiB, through the SSRF guard. Internal agents are allowed per host with
discovery.a2a_private_hosts(plain http allowed for those hosts only). - Imported entries are not listed in
/.well-known/ard.json: the manifest describes this publisher’s own resources. - Both A2A v1.0 cards (
supportedInterfaces) and v0.3 cards (url,preferredTransport,additionalInterfaces) are accepted. The client speaks the JSON-RPC binding, with v0.3 method names for 0.x agents.
Decision 4: No proxy; credentials stay with the caller
The CLI calls a remote agent directly. The server never relays A2A traffic (consistent with “remote URLs remain direct” in AGENTS.md). Observal never stores agent credentials: the client reads a token fromOBSERVAL_A2A_TOKEN_<HOST>, named after the host of the endpoint it is sent to
(agents.acme.com reads OBSERVAL_A2A_TOKEN_AGENTS_ACME_COM; - becomes
__ so two hosts never share a name), and sends it as the card’s security
scheme asks. There is no catch-all variable: a token only ever reaches the host
it is named for.
Decision 5: How agents reach delegation
Every Agent installed withobserval agent pull gets an observal-agents MCP
server (python -m observal_cli.delegation.mcp_server) with four tools:
find_agents, delegate, get_task, cancel_task. MCP is the one
integration point every harness has, so this needs no per-harness work. The
discovery.delegation_enabled setting (default on) controls the injection.
The entry carries no agent id, because one observal-agents entry is shared by
every agent pulled into the same project or user config. The server takes the
calling agent from OBSERVAL_AGENT_ID, which a delegated child always has.
delegate waits at most 50 seconds, below common MCP tool timeouts (Codex:
60 s), and returns a task_id that get_task follows.
People and scripts use observal delegate find|run|status|reply|list|cancel.
The harness’s own tool-permission prompt for delegate is the consent point
for starting a delegation.
Decision 6: Guards
Checked before anything runs:- only approved entries whose search result says
obs:delegable: true; - depth: a delegated agent may delegate again, up to
OBSERVAL_DELEGATION_MAX_DEPTHlevels (default 2), carried to children inOBSERVAL_DELEGATION_DEPTH; - breadth: a delegated agent runs headless with no permission prompt, so it
may start at most three tasks of its own (counted by
parentTaskId); - cycles: an agent already in
OBSERVAL_DELEGATION_CHAIN(by identifier and by agent id), or the caller itself, is refused.
Decision 7: Delegability is its own field
Search results carryobs:delegable and, for remote agents,
obs:availability: delegate. Neither is blended into score (ADR 0001,
Decision 8). A registry Agent is delegable when it is approved and at least
one harness it supports has headless_run.
Decision 8: Telemetry through existing channels
A delegation appends a capability-lock line in the caller’s context (mode = delegated, kind agent or external, with the task id and child
harness). Session upload already attaches those lines to the caller’s session,
so traces show which agents a session delegated to. The child’s own session is
ingested by its harness hooks as usual; Claude Code children run with a known
--session-id recorded on the task. The task store indexes the child’s session
id and workspace, and session upload checks that index first, so the child’s
session is attributed to the delegated agent and version even when it is
delivered after the workspace is gone (observal reconcile, recovery after a
crash). The user’s lockfile is never touched. No OTLP, no new ingestion path.
Amendment to ADR 0001, Decision 7
ADR 0001 said an Observal Agent is never emitted asapplication/a2a-agent-card+json. That still holds: registry Agents keep
application/vnd.observal.agent+json. What changes is that Observal now
ingests A2A cards as a governed resource type and can call both kinds of
agent through one A2A-shaped task model.
Deferred
- Serving registry Agents as A2A endpoints (
observal a2a serve) so outside frameworks can call them. The task model is ready for it. - Streaming (
SendStreamingMessage) and push notifications; the client polls. - Verifying signed Agent Cards.
- Linking parent and child sessions server-side beyond the capability lock.
- Headless support for Goose once verified against real installs.

