Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions openspec/changes/confirm-sender/design.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions openspec/changes/confirm-sender/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions openspec/changes/confirm-sender/specs/peer-sender-identity/spec.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions openspec/changes/confirm-sender/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
Loading