1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
# 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 <message-file>
```
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 <N>` | 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 `<message-file>.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 <msg>.asc <msg>` 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.
|