diff --git a/openspec/changes/confirm-sender/design.md b/openspec/changes/confirm-sender/design.md new file mode 100644 index 000000000000..e5f002b21318 --- /dev/null +++ b/openspec/changes/confirm-sender/design.md @@ -0,0 +1,17 @@ +## Decisions + +**D1 Callback, not credentials.** No peer-credential primitive exists in the runtime, so identity is +established by asking the claimed sender, who alone holds the answer. A forger cannot make the real +session report a message it never sent. + +**D2 Fail to unattributed, never to attributed.** No answer (timeout, dead socket, unknown id) means +the message arrives with the sender unverified. Delivery is never blocked by the check. + +**D3 The sender log is in memory and bounded** (recent ids, short TTL). It is not a record to audit +and holds no message text. + +**D4 Claude Code senders are out of scope** because they cannot be asked. For them the lead check keeps +matching pid and address, and docs must call that a claim. + +**D5 Replay.** A confirmation names the receiver and the `msg_id`; a sender answers yes only for ids it +sent to that receiver, so a confirmed id cannot vouch for a different recipient. diff --git a/openspec/changes/confirm-sender/proposal.md b/openspec/changes/confirm-sender/proposal.md new file mode 100644 index 000000000000..b5449d3ae1e7 --- /dev/null +++ b/openspec/changes/confirm-sender/proposal.md @@ -0,0 +1,35 @@ +# Confirm the sender of a peer message by calling it back + +Parent: `autonomous-swarm`; unblocks `review-on-done` Phase 1b. Found in review of the lead +grant (2026-10-03). + +## Why + +An inbound peer message names its sender in an envelope attribute (`from`). The receiving +session turns that attribute into a session id (`resolveOpencodeSender`) and `verifyLead` compares +it with the lead grant. The attribute is written by the sender, so any local process that can +reach the target's socket can claim to be the lead. `codec.ts` already says provenance must never +come from these fields; the code does not follow it. Bun exposes no peer credentials on a unix +socket (probed on 1.3.14: `getPeerName`, `getPeerCredentials`, `getPeerCert` are all undefined), so +the transport cannot authenticate the sender. + +## What Changes + +1. Before a message is attributed to an opencode session, the receiver asks the claimed sender's + own sidecar "did you send `msg_id` X to me?" and attributes the message only on a yes. +2. A sender keeps a short in-process log of the `msg_id`s it sent and to whom, and answers + from that log. +3. An unconfirmed claim is delivered as an unattributed message: text is kept, the sender is + shown as unverified, and no lead authority is granted. + +## What this does NOT do + +This **bounds** sender identity; it does not make it unforgeable. + +- A same-user process that can replace a registry entry or a socket can still answer for it. Against + a same-user, shell-capable agent nothing short of OS-level separation is unforgeable. +- It covers opencode senders only. A Claude Code session cannot answer the callback, so a Claude Code + lead remains a claim (matched on pid and address). This must be stated wherever a Claude Code lead + is described as verified. +- The real controls stay what they were: a human-confirmed grant, scopes, the publish never-list, and + the rule that a review record is written by a tool from a session id it holds, never from message text. diff --git a/openspec/changes/confirm-sender/specs/peer-sender-identity/spec.md b/openspec/changes/confirm-sender/specs/peer-sender-identity/spec.md new file mode 100644 index 000000000000..06abd1e35ab3 --- /dev/null +++ b/openspec/changes/confirm-sender/specs/peer-sender-identity/spec.md @@ -0,0 +1,13 @@ +## ADDED Requirements + +### Requirement: an opencode sender is attributed only after it confirms the message +The system SHALL attribute an inbound peer message to an opencode session only when that session's own +sidecar confirms it sent that message id to this receiver. + +#### Scenario: forged sender +- **WHEN** a message claims to be from another session that never sent it +- **THEN** it is delivered unattributed and grants no lead authority + +#### Scenario: no answer +- **WHEN** the claimed sender does not answer in time +- **THEN** the message is delivered with the sender marked unverified, not dropped diff --git a/openspec/changes/confirm-sender/tasks.md b/openspec/changes/confirm-sender/tasks.md new file mode 100644 index 000000000000..b844d8fba80e --- /dev/null +++ b/openspec/changes/confirm-sender/tasks.md @@ -0,0 +1,12 @@ +## 1. Tests first +- [ ] 1.1 A forged `from` naming another opencode session is attributed to nobody. Assert both halves: verifyLead (lead.ts:166) denies it, AND the delivery-attribution caller (lifecycle.ts:122) does not treat the unattributed result as authoritative. +- [ ] 1.2 A genuine message is confirmed and attributed; the lead grant works end to end. +- [ ] 1.3 No answer / dead socket / wrong receiver all yield unattributed delivery, not a drop. + +## 2. Implementation +- [ ] 2.1 `confirm` frame and handler in the sidecar; bounded sent-log recorded on send. The sent-log key is (msg_id, receiver_session_id) where receiver_session_id is the target the sender dialed at send time — not derived from the return path. The confirm must match both fields; a confirm on the right socket with the wrong receiver is a miss, not a hit. +- [ ] 2.2 Receiver calls back before `resolveOpencodeSender` result is trusted. +- [ ] 2.3 Show unverified senders as such to the receiving agent. + +## 3. Docs +- [ ] 3.1 State the same-user residual and the Claude Code limitation in the lead docs and WoW.