# cross-agent-recv **Purpose.** The canonical receiver-side processor. Reads a single incoming message file and reports a structured decision the agent acts on: process / dedup / query / reject. The script handles only mechanical checks (frontmatter, signature, dedup, version, tools). Substance-level decisions like `pushback` ("I disagree with this request") happen one layer up — after the agent reads the message body the script returns as `process`-able. This is the read-side counterpart to `cross-agent-send`. Together they are the two halves of the per-message contract. The agent's polling loop calls `cross-agent-recv` on every new file in `inbox/from-agents/` and dispatches on the decision. Without this script, every receiver implementation re-invents GPG verify + frontmatter sanity-check + SHA-256 dedup. With it, behavior is consistent across projects. ## Usage ``` cross-agent-recv ``` Single positional argument: a `.org` file in `inbox/from-agents/`. The matching `.asc` signature file must be present alongside it. ### Flags | Flag | Default | Purpose | |---|---|---| | `--no-verify` | (verify on) | Skip GPG verification. Testing only. | | `--no-dedup` | (dedup on) | Skip SHA-256 dedup against existing files. Testing only. | | `--protocol-version ` | 5 | Override the expected protocol version. Useful for testing forward-compatibility checks. | | `--json` | off | Output decision as JSON for easier parsing by the agent. | ## Behavior Runs the receiver checks in order. First failure determines the decision. ### Step 1 — Frontmatter sanity-check Parse the message's org-mode frontmatter. Required fields: - `#+TITLE` - `#+CONVERSATION_ID` - `#+MESSAGE_TYPE` (must be one of: `request`, `progress`, `query`, `pushback`, `complete`, `release`, `escalate`) - `#+SEQUENCE` (integer) - `#+TIMESTAMP` (ISO 8601 with explicit offset) - `#+PROTOCOL_VERSION` (must match the expected version; default 5) Any required field missing, malformed, or the protocol version mismatched → decision = `reject` (frontmatter) or `query` (version mismatch — see below). ### Step 2 — Protocol-version check If `PROTOCOL_VERSION` doesn't match the expected: - Decision = `query`. Action: receiver should write a `query` reply asking the sender to upgrade to the expected protocol version. ### Step 3 — Signature verification Look for `.asc` alongside the `.org`. If missing or `gpg --verify` fails: - Decision = `reject` (signature). Surface to user; do not act. The `.asc` file MUST be present when the `.org` is — `cross-agent-send` guarantees this with its strict ordering (`.asc` lands first). If the `.asc` is missing despite the `.org` being present, the sender violated atomic-write ordering or the file was tampered with in transit. ### Step 4 — SHA-256 dedup Compute SHA-256 of the message file. Scan the same directory for existing files matching `CONVERSATION_ID + SEQUENCE`: - No match → decision = `process` (new message, dispatch by type). - Match with **identical** SHA-256 → decision = `dedup` (silent retry; do not reprocess). - Match with **different** SHA-256 → decision = `process` (sequence collision with non-identical content; both are legitimate, ordered by `#+TIMESTAMP`). ### Step 5 — REQUIRES_TOOLS optional check If the message has a `#+REQUIRES_TOOLS` field, verify each named tool/MCP is available in the receiver's environment. - All available → `process`. - One or more missing → decision = `query`. The agent should write a `query` reply naming the missing tools, asking the sender to reframe the request to avoid them. ### Step 6 — Dispatch decision If all checks pass, decision = `process` with the parsed `MESSAGE_TYPE` so the agent's main loop knows which handler to invoke. ## Output ### Default (human-readable) ``` $ cross-agent-recv inbox/from-agents/20260427T091015Z-from-homelab-prep-fixup.org decision: process message_type: request conversation_id: prep-fixup sequence: 6 sha256: a1b2c3d4... ``` ### `--json` ```json { "decision": "process", "reason": null, "message_type": "request", "conversation_id": "prep-fixup", "sequence": 6, "timestamp": "2026-04-27T04:11:42-05:00", "sha256": "a1b2c3d4..." } ``` For decisions other than `process`, `reason` carries a human-readable explanation: ```json { "decision": "query", "reason": "PROTOCOL_VERSION mismatch: expected 5, got 4", "conversation_id": "prep-fixup", "sequence": 6 } ``` ## Decision exit codes | Decision | Exit code | Agent action | |---|---|---| | `process` | 0 | Dispatch to the message-type handler | | `dedup` | 1 | Silent — do nothing further | | `query` | 2 | Write a `query` reply (see `reason` for what to ask) | | `reject` | 3 | Surface to user; do not auto-reply | The agent reads stdout/JSON to learn the decision; it can also key off exit code for simpler bash-style dispatching. ## Failure modes | Symptom | Cause | Fix | |---|---|---| | `decision: reject (frontmatter)` | Required field missing or malformed | Open the message; fix or surface to user. The sender should not have produced this file. | | `decision: reject (signature)` | `.asc` missing, GPG verify failed, or signer unknown | Check that `.asc` exists alongside `.org`. If yes, run `gpg --verify .asc ` manually for diagnostic output. | | `decision: query (PROTOCOL_VERSION)` | Sender on older/newer protocol | Reply with a `query` asking sender to upgrade. Both sides should align before continuing. | | `decision: query (REQUIRES_TOOLS)` | Receiver lacks one of the named tools | Reply with a `query` naming the missing tools; sender should reframe to avoid. | | `decision: dedup` | Already-processed identical retry | No action. The script handled it correctly. | ## HALT awareness Checks `~/.config/cross-agent-comms/HALT` at the start of every invocation. If HALT exists, exits with code 5 ("halt active; remove ~/.config/cross-agent-comms/HALT to resume") without verifying, deduping, or returning a decision. **The inbound file is left in place** — not moved, not rejected, not deduped. When HALT clears and polling resumes, the file gets picked up via the normal cold-start handling (whichever surfaces first: watcher notification, startup workflow check, or the next agent poll). Reversibility is preserved. If the HALT file exists but is unreadable, fail-closed — treat as if HALT is set. See `cross-agent-halt.md` for the full halt mechanism. ## Examples ```bash # Basic invocation in an agent's polling loop for msg in inbox/from-agents/*.org; do decision=$(cross-agent-recv --json "$msg") case "$(echo "$decision" | jq -r '.decision')" in process) handle_message "$msg" ;; dedup) ;; # silent query) write_query_reply "$msg" "$decision" ;; reject) surface_to_user "$msg" "$decision" ;; esac done # Test signature verification only cross-agent-recv --no-dedup inbox/from-agents/test-msg.org # Test against a future protocol version cross-agent-recv --protocol-version 6 inbox/from-agents/future-msg.org ``` ## Performance The script is fast (single SHA-256 compute, single GPG verify, frontmatter parse). For typical messages (single-digit KB), runs in well under 100ms. Dedup-scan is O(N) over files in the directory; if a project's `inbox/from-agents/` accumulates hundreds of files, archive released conversations to keep the scan fast. ## See also - `cross-agent-send` — counterpart writer. - `cross-agent-watch` — fires when a new message arrives; agent then calls `cross-agent-recv` to process it. - `cross-agent-status` — pending-message snapshot (uses similar released-vs-unreleased logic, but doesn't process individual messages). - `cross-agent-comms.org` — protocol spec, the "what" the script implements.