Skip to content

feat(a2a): let scripts send messages as registered machine participants - #144

Merged
Jacksondr5 merged 1 commit into
j5/mainfrom
j5/a2a-machine-senders
Sep 15, 2026
Merged

Jacksondr5 merged 1 commit into
j5/mainfrom
j5/a2a-machine-senders

Conversation

@Jacksondr5

Copy link
Copy Markdown
Owner

Closes #74.

Problem

Nothing outside an agent session could send an A2A message. The monitoring fleet's launchd/systemd watchdogs relayed wakes through a file outbox drained by a courier agent, which died with its session and attributed every message to itself instead of the watchdog.

Fix

A third participant kind, the machine participant: a registered non-agent sender with an immutable Squadron home and no thread. It sends plain messages only and never receives, so no Exchange or silence can involve it.

  • CLI j5 a2a send | list | whoami | participant create | token issue, with the exit codes from the requirements (0 ok, 2 usage, 3 unauthenticated, 4 recipient not found or ambiguous, 5 refused by policy, 6 unreachable), --json everywhere, @file/- message input, origin and token from flags or J5_ORIGIN / J5_TOKEN / J5_TOKEN_FILE, and a 2 s default timeout. It never prompts.
  • Routes under /api/j5/a2a/{send,roster,whoami,machine-participants} through the existing J5 route aggregate. Send drives the same SendService path and command-id replay rules as the MCP send_message tool, so retrying with one --client-request-id returns the original receipt. --to accepts a participant id, a thread id, or an agent's display name; an ambiguous name answers 409 with candidates. The roster carries measured liveness from the thread shell snapshot.
  • Identity is the token's session subject. j5 a2a token issue --participant <name> mints a session with only the new a2a:send scope and subject machine:<name>; the routes authorize on both. There is no --as flag, so one token cannot send as another machine.
  • Upstream edits (FORK.md case 35, Jackson's 2026-09-15 ruling): the a2a:send literal appended to AuthEnvironmentScope in packages/contracts/src/auth.ts, outside both client bundles, and the a2aCommand append in apps/server/src/bin.ts. Everything else is add-beside, including migration 13 and the machine kind in J5-owned contracts.
  • Rendering: a machine message gets its own envelope (version 18) telling the agent the sender is automated and cannot receive a reply; the web card shows the machine's registered name with an Automation tag.
  • Docs: the A2A definition (participants, envelopes, AC24–AC28, a scraper scenario), glossary, agent-tools, a new operator runbook docs/j5/runbooks/machine-senders.md, and the A2A README.

Deliberately out of scope: asks/replies/urgency from machines (no reply sink exists), and archiving or renaming a machine participant (revoking its token is the current way out).

Verification

  • Typecheck clean for apps/server, apps/web, packages/contracts; lint clean on changed paths.
  • vp test run over the touched A2A files: 15 files, 83 tests. New coverage: participant registration and replay, machine send commit/replay and refusals, roster liveness and recipient resolution, every route's auth and error mapping, and the CLI against a stub HTTP server for each exit code. Web renderer: 39 tests including the machine envelope card.
  • Not yet done: the end-to-end acceptance run from the requirements against a live fleet, and a before/after screenshot of the Automation tag on the thread card (requires a browser pass).

Built by Claude Fable 5.1 in Claude Code.

🤖 Generated with Claude Code

Nothing outside an agent session could send an A2A message, so the monitoring
fleet's watchdogs relayed wakes through a file outbox and a courier agent that
died with its session and attributed every message to itself (#74).

Adds a third participant kind, the machine participant: a registered non-agent
sender with a Squadron home and no thread that sends plain messages and never
receives. A new `j5 a2a` CLI (send, list with liveness, whoami, participant
create, token issue) talks to new authenticated routes that drive the same send
service and command-id replay rules as the MCP tool. Identity comes from a token
bound to the participant through its session subject and the new `a2a:send`
scope, appended to the upstream scope list outside both client bundles (FORK.md
case 35). Machine messages get their own envelope and an Automation tag on the
thread card. Definitions, glossary, and an operator runbook are updated.

Built by Claude Fable 5.1 in Claude Code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). labels Sep 15, 2026
@Jacksondr5

Copy link
Copy Markdown
Owner Author

Consumer review (Production Monitoring Director, on Jackson's behalf): meets the requirements in j5-cli-requirements.md rev 2 — and is stricter in two places I'm glad about.

  • send: --to by id / thread / exact display name with ambiguity → exit 4; --message text / @file / -; --client-request-id required (better than my optional); replay through the same command-id shape as the MCP tool; exit codes exactly as specified; 2 s default → exit 6; never prompts. ✔
  • list: idle/active/errored from the thread shell plus last run start/end and last error in --json — sufficient for watchd's reachability preflight. ✔
  • whoami: exit 3 semantics as specified. ✔
  • Auth: identity from the token subject (machine:<name>) with a single a2a:send scope and no --as — stronger than my draft; sessions persist across restarts and are revocable, which is the property the in-session MCP bridge lacked. ✔

Two acceptable design choices worth stating in the runbook for whoever wires escalations: (1) machines cannot ask a person (exit 5), so a human-facing escalation needs one agent hop (machine → Director → ask); (2) --ttl 365d implies a yearly rotation task.

Remaining acceptance (the end-to-end run against a live fleet) I'll do on the monitoring server after merge and runtime update: register watchdog, mint a token, send a canary to obs-sentinel, verify attribution and no-duplicate replay, stop the server and confirm exit 6 within 2 s. Will report back here.

— Posted by the Production Monitoring Director agent (J5) on Jackson's behalf.

@Jacksondr5
Jacksondr5 marked this pull request as ready for review September 15, 2026 21:31
@Jacksondr5
Jacksondr5 merged commit f546353 into j5/main Sep 15, 2026
24 checks passed
@Jacksondr5
Jacksondr5 deleted the j5/a2a-machine-senders branch September 15, 2026 21:31
@Jacksondr5

Copy link
Copy Markdown
Owner Author

End-to-end acceptance: PASS — run 2026-09-16 04:02–04:05Z against the live J5 Code desktop app 0.0.40 (laptop, port 3773), Squadron "Production Monitoring", from a shell with no agent session.

Step Result
participant create --squadron … --name watchdog-acceptance (server host, no token) created:true, id machine:watchdog-acceptance
token issue --participant watchdog-acceptance --ttl 1d --token-only 308-char token, single line
whoami exit 0; participant, squadron, server 0.0.40
list --json 14 participants, kinds agent/human/machine; liveness {state, runStatus, latestRunStartedAt, …} present
send --to <agent id> --client-request-id X exit 0, sender: machine:watchdog-acceptance, messageId returned
replay, same id exit 0, identical messageId; DB: 1 j5_a2a_delivery row for the id
unknown recipient exit 4 recipient_not_found
human recipient (plain send) exit 5 policy_refused
bad token exit 3 http_401
unreachable origin exit 6 in 0.47 s
GET on /api/j5/a2a/send 200 text/html (SPA fallthrough); POST without token → 401 ✔

Two CLI defects found along the way (neither blocks us):

  1. Usage errors print help to stdout. token issue given an unknown flag (--origin — it runs against the local auth DB, so the flag is invalid) exits 2 but writes the full help text to stdout; with --token-only in a $(…) capture that help text became the "token". Usage output should go to stderr, and --token-only should emit nothing on stdout when it fails.
  2. Malformed bearer → exit 6. With that multi-line garbage as the token, every verb reported server_unreachable (exit 6, "Transport error") because the invalid header value made fetch throw before any request. A header-construction failure should map to exit 3 (or 2), not 6 — scripts treat 6 as "queue and retry".

Machine participants can't be archived, so machine:watchdog-acceptance remains registered in the Squadron (harmless); its sessions are revoked.

— Posted by the Production Monitoring Director agent (J5) on Jackson's behalf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A: external send path for machine senders (launchd watchdog delivery)

1 participant