# cross-agent-send **Purpose.** Send a cross-agent message file to a specific destination. Handles peer-config lookup, GPG signing, atomic write (same-machine) or rsync push (cross-machine), retry-with-backoff, and failure surfacing. This is the canonical writer. The protocol spec defers all writer mechanics to this script. ## Usage ``` cross-agent-send [--no-sign] [--retries N] ``` ### Positional arguments | Position | Meaning | Example | |---|---|---| | 1 | Destination as `.` | `homelab.career`, `velox.career` | | 2 | Message file (already-formatted `.org`) | `/tmp/my-message.org` | ### Flags | Flag | Default | Purpose | |---|---|---| | `--no-sign` | (signing on) | Skip GPG signing. Use only for testing; receivers reject unsigned messages by default. | | `--retries N` | 3 | Override retry count for cross-machine sends. | | `--key ` | (user's primary key) | GPG key to sign with. Resolution order: `--key` flag, `GPG_USER` env, `git config user.signingkey`, then the first secret key in the keyring. | ## Behavior ### Filename generation (script-controlled) The script generates the canonical destination filename from the message's frontmatter and sender context. The user's input filename is ignored — pass any path, the script names the destination correctly: ``` TZ-from--.org ``` `` comes from the sender machine's project name (config or hostname-based). `` is read from the message's `#+CONVERSATION_ID` frontmatter field. UTC timestamp is generated at send time. The script also performs the **sender-side max-seen scan** before writing: it reads the receiver's `from-agents/` directory, finds the highest existing sequence in this conversation across both sender prefixes, and (best-effort) suggests `max(seen) + 1` for the next sequence. The user/agent is responsible for setting `#+SEQUENCE` in the message body; the script only advises. ### Same-machine destinations Resolved when the destination's machine matches the current hostname (or is not in `peers.toml` as a remote). Steps: 1. Parse frontmatter; extract `CONVERSATION_ID` and `TIMESTAMP`. Validate per the *Validation before send* section below. 2. Generate canonical filename per *Filename generation* above. 3. Sign: `gpg --detach-sign --armor --output .asc --local-user `. 4. Compute target: read `peers.toml` for the project's `inbox_path`. If missing, fall back to `~/projects//inbox/from-agents/`. 5. **Atomic write with strict ordering** (signature must precede message): - Stage `.asc`: write to `/.tmp.XXXXXX-.asc`, then `mv` to `/.asc`. - **Then** stage `.org`: write to `/.tmp.XXXXXX-`, then `mv` to `/`. - Receivers only act on `.org` files; staging the `.asc` first guarantees the signature is present when the receiver opens the message. Out-of-order would race: receiver could read the `.org` before the `.asc` lands and fail GPG verify even though the sender did everything right. 6. Exit 0 on success. Exit non-zero if any step fails. ### Cross-machine destinations Steps: 1. Parse + generate canonical filename, as same-machine steps 1-2. 2. Sign locally to `.asc` (or a tmp staging file). 3. rsync push **with the same .asc-first ordering**: - `rsync -a .asc @:/.asc` - **Then** `rsync -a @:/` rsync writes to a hidden temp file then renames atomically by default (`--inplace` would defeat this; do not pass it). 4. Retry on failure: 5s, 30s, 120s backoff, then surface error. 5. On persistent failure: write a marker file to `~/.local/state/cross-agent-comms/failed-sends/--.json` containing the destination, message path, error, and retry log. Exit non-zero. ### Validation before send - Destination resolves via `peers.toml` (or local fallback). If neither, exit immediately with `destination not found in peers.toml; available: `. - Message file must be readable, non-empty, and have valid org-mode frontmatter with **all** of the following required fields: - `#+TITLE` - `#+CONVERSATION_ID` - `#+MESSAGE_TYPE` - `#+SEQUENCE` - `#+TIMESTAMP` - `#+PROTOCOL_VERSION` (must equal `5` for v5) If any required field is missing or malformed, exit immediately with a parse error naming the offending field. - Optional fields the script recognizes and passes through (no special handling beyond preservation): - `#+REQUIRES_TOOLS` — comma-separated tool/MCP slugs the receiver needs. - `#+RELEASE_STATUS` — valid only on `MESSAGE_TYPE: release`. Values per spec: `complete`, `cancelled`, `withdrawn-after-pushback`, `abandoned-after-escalation`. - `#+WORKFLOW_VERSION` — sender's version of the cross-agent-comms workflow file. Currently advisory; receiver may warn on mismatch but does not block. ## Configuration Reads `~/.config/cross-agent-comms/peers.toml` for peer routing: ```toml [peers.velox] host = "velox.local" ssh_user = "cjennings" # Optional: per-project inbox-path overrides for non-default layouts. [projects.work] inbox_path = "~/projects/work/inbox/from-agents" [projects.homelab] inbox_path = "~/projects/homelab/inbox/from-agents" ``` If a project entry is omitted, defaults to `~/projects//inbox/from-agents`. ## Failure modes | Symptom | Cause | Fix | |---|---|---| | `destination not found in peers.toml` | Misspelled destination, or peer not configured | Run `cross-agent-discover` to see available destinations. | | `signing failed: no secret key` | GPG key missing or not in keyring | `gpg --list-secret-keys` to confirm. Override with `--key `. | | `signing failed: pinentry timed out` | Headless session, GUI pinentry unavailable | Confirm `pinentry-program` in `gpg-agent.conf` matches available pinentry. Per protocols.org, GUI pinentry works from Claude Code. | | `rsync exit 255` | SSH unreachable | `cross-agent-discover --peer ` to confirm reachability. | | `rsync exit 23` | Permission denied at destination | Check destination directory perms (`chmod 700`) and ownership. | | Marker file written to `failed-sends/` | Persistent cross-machine failure | Inspect the marker's `error` field. After fixing, retry: `cross-agent-send ` (the marker is for visibility; it does not auto-retry). | | Receiver complains "unsigned message" | `--no-sign` was used in production | Don't use `--no-sign` outside testing. | ## HALT awareness Checks `~/.config/cross-agent-comms/HALT` at the start of every send AND between the `.asc` and `.org` rsync calls AND between each retry iteration. On HALT exists, exits with code 5 ("halt active; remove ~/.config/cross-agent-comms/HALT to resume") without writing or pushing further. Worst case: one in-flight send completes its current rsync step within a few seconds before halt kicks in for the next step. New sends are blocked immediately. No `pkill` needed — the per-iteration check stops things naturally. If the HALT file exists but is unreadable (permissions wrong), fail-closed — treat as if HALT is set. Safer than fail-open. See `cross-agent-halt.md` for the full halt mechanism. ## Examples ```bash # Same-machine send cross-agent-send homelab.career /tmp/my-message.org # Cross-machine send via Tailscale cross-agent-send velox.career /tmp/my-message.org # Test send without signing (receiver will reject) cross-agent-send homelab.career /tmp/test.org --no-sign # Override retry count for a flaky link cross-agent-send velox.career /tmp/my-message.org --retries 10 # After a delivery failure, inspect the marker cat ~/.local/state/cross-agent-comms/failed-sends/*.json | jq . ``` ## Exit codes | Code | Meaning | |---|---| | 0 | Sent successfully. | | 1 | General error (parse failure, signing failure, etc.). | | 2 | Destination not found in peers.toml. | | 3 | Cross-machine delivery failed after retries. Marker file written. | | 4 | Frontmatter validation failed. | ## See also - `cross-agent-discover` — validate destinations before sending. - `cross-agent-watch` — receiver-side notification. - `cross-agent-status` — see what's queued. - `cross-agent-comms.org` — protocol spec, the "what" the script implements.