# Codex Handoff — v0.5.9 implementation batch
**Branch:** `codex/v0.5.9-batch` (create from `main`)
**Target:** ship all 8 tasks below as separate commits on that branch
**Repo:** `SlanchaAi/wire` @ HEAD (`ff22a18` as of this writing)
**Audience:** Codex (OpenAI). Single agent doing serial implementation work.
This doc captures what to build, how to verify each piece, and where the
judgment calls are. Each task is self-contained — Codex should be able to
tackle them in the listed order without needing additional context beyond
this file + the in-repo code.
## Before starting (one-time setup)
```bash
git checkout main
git pull --ff-only
git checkout -b codex/v0.5.9-batch
```
Confirm baseline:
```bash
cargo build --release # should succeed
cargo test --release # full suite, expect green except possibly
# tests/e2e_detached_pair.rs (pre-existing
# local-only flake; CI is green on it)
cargo fmt --all --check # must be clean
cargo clippy --release # warnings ok, no errors
```
If any of those fail before you start, stop. Don't begin work on a broken
baseline. Open an issue / report back instead.
## House rules
- **One commit per task.** Atomic. Each commit message starts with the
task ID in brackets (e.g. `[a] migrate A2A extension URI to slancha.ai`).
- **Tests pass before commit.** Run `cargo test --release` + `cargo fmt
--all --check` + `cargo clippy --release` before each commit. If a task
modifies tests, the modification must be defensible (matching schema
change, not loosening the assertion to make a bug pass).
- **No version bump per task.** Only the final commit on this branch bumps
`Cargo.toml` + `tests/cli.rs` version assertion + `CHANGELOG.md` to
`v0.5.9`. See task `final` below.
- **Don't touch unrelated code.** Each task's diff should be reviewable in
isolation. If a refactor would help, leave a comment in the commit body
and skip it — don't bundle.
- **Phyllis voice on new user-facing strings.** Style established in
v0.5.4. Examples: `phyllis: <peer>'s line is silent`, `phyllis: that
number's been disconnected`. Tone = sixties-switchboard-operator,
technical content preserved.
- **A2A extension URI stays exact-string-matched.** See task `a` —
there's one URI the protocol treats as an opaque identifier. Don't
generalize the comparison even when migrating the string.
- **If blocked:** add a `## Blocked` section to a `BLOCKED.md` file at
repo root with what stopped you + what info you'd need to unstick.
Continue with the next task. Don't sit.
## Task order (suggested)
Tasks ordered cheapest-first to build confidence. Each task notes
dependencies on prior tasks.
---
## [a] Migrate A2A extension URI from laulpogan to slancha.ai
**Why:** wire's A2A extension URI is currently
`https://github.com/laulpogan/wire/ext/v0.5`. The repo moved to
`SlanchaAi/wire`. The URI is opaque-identifier-matched by federation
peers, and right now wire is the only consumer in the wild, so migration
is free. We want the namespace under a stable domain (slancha.ai owned by
the org) rather than tied to GitHub specifically.
**Target URI:** `https://slancha.ai/wire/ext/v0.5`
**Files to touch:**
- `src/relay_server.rs` — find the line containing
`"uri": "https://github.com/laulpogan/wire/ext/v0.5",` (inside the
`well_known_agent_card_a2a` handler). Update to new URI. Update the
preceding block-comment to drop the "MUST stay forever as laulpogan"
framing — that was overcautious. Replace with a short note that
extension URIs are opaque namespace identifiers and changing them is
a coordinated federation-spec bump.
- `src/pair_profile.rs` — find the line containing
`.starts_with("https://github.com/laulpogan/wire/ext")`. Update to
match the new prefix. Drop the matching comment.
- `CHANGELOG.md` — add a line under the v0.5.9 section (you'll create
v0.5.9 in the `final` task) noting the migration. Skip until the
`final` task creates the section.
**Test command:**
```bash
cargo test --release --test relay
cargo test --release --lib agent_card
```
**Acceptance:**
- Both file edits applied. `grep -rn "laulpogan/wire/ext" src/` returns 0
matches.
- `cargo test --release` full suite green.
- The well-known-agent-card-a2a endpoint now returns
`extensions[0].uri = "https://slancha.ai/wire/ext/v0.5"`. Verify with a
unit test or smoke against a local relay:
```bash
cargo run --release --bin wire -- relay-server --bind 127.0.0.1:18770 &
PID=$!
sleep 1
kill $PID
```
**Non-goals:**
- Don't add a transition-window "accept both old and new URI" matcher.
Wire is pre-traction; no external consumers depend on the old URI.
Clean cut.
- Don't update `docs/MAC_HANDOFF_2026_05_12.md` or
`docs/CLOUD_MIGRATION.md` references to the old URI — those are
historical context, not live spec.
**Commit message:**
```
[a] migrate A2A extension URI to slancha.ai namespace
The extension URI is an opaque identifier matched by federation peers,
not a forwardable URL. Wire is the only consumer in the wild at v0.5,
so the migration is free. Targeting slancha.ai (the org domain) instead
of github.com/laulpogan decouples the namespace from any future repo
moves.
- src/relay_server.rs: extension URI in AgentCard
- src/pair_profile.rs: matcher prefix in A2A extension recognition
Replaces the overcautious "MUST stay forever" comment in both files
with a note that extension URIs are namespace identifiers and changing
them is a coordinated federation-spec bump.
```
---
## [b] Lifetime handle counter (split first-claims from re-claims)
**Why:** `RelayCounters.handle_claims_total` currently increments on
every `POST /v1/handle/claim` including re-claims by the same DID (which
happen on profile rotation / slot rotation). For the public `/stats`
endpoint, the more meaningful number is "how many distinct handles have
ever been claimed" — first-claims only. Re-claims tell us about
operator activity, not user growth.
**Files to touch:**
- `src/relay_server.rs`:
- Add `handle_first_claims_total: AtomicU64` to `RelayCounters` struct.
- Add same to `CountersSnapshot` struct + serde derive.
- In `handle_claim` handler: increment `handle_claims_total` on every
request (existing behavior); additionally increment
`handle_first_claims_total` ONLY when the response status is
"claimed" (i.e., the nick was not already in the handles directory).
Don't increment on same-DID re-claims (status="re-claimed") or 409
conflicts.
- In `GET /stats` JSON response: surface
`handle_first_claims_total` alongside `handle_claims_total`.
- In `Relay::new` (counter-loading path): also load
`handle_first_claims_total` from `counters.json`, default 0 for
pre-v0.5.9 snapshots.
- In `persist_counters`: include the new field in the serialized
snapshot.
**Test command:**
```bash
cargo test --release --test relay
cargo test --release --lib
```
**Acceptance:**
- Add or extend a relay test that claims a nick once + verifies the
counters JSON shows both `handle_claims_total = 1` and
`handle_first_claims_total = 1`.
- Then runs a same-DID re-claim and verifies `handle_claims_total = 2`
but `handle_first_claims_total = 1`.
- Smoke against a running local relay:
```bash
curl http://127.0.0.1:18770/stats
```
**Non-goals:**
- Don't add a per-handle history log — aggregate counter only.
- Don't worry about migration of `counters.json` from pre-v0.5.9
installs. The default-0-if-missing handling in `Relay::new` covers it.
**Commit message:**
```
[b] /stats: split handle_first_claims_total from handle_claims_total
handle_claims_total now counts every POST /v1/handle/claim including
profile-rotation re-claims by the same DID. The more interesting public
metric for "user growth" is first-claims only. Adds the new counter
alongside the existing one (don't break the old number — both are
useful).
- src/relay_server.rs: RelayCounters + CountersSnapshot + handle_claim
+ /stats handler + persist/load paths
- tests/relay.rs: test asserting re-claim increments
handle_claims_total but not handle_first_claims_total
```
---
## [c] R2: `time_sensitive_until` event field + `--deadline` CLI flag
**Why:** From `docs/INCIDENT_REPORT_2026_05_12_AGENT_ATTENTION_LAYER.md`
recommendation R2. Today's `priority: low|normal|high` is too coarse and
not bound to wall-clock deadlines. Adding an optional `time_sensitive_until`
RFC 3339 timestamp lets receivers distinguish "ack within 30 min for v8
smoke train" from "ack whenever." Receiver-side handling is advisory in
v0.5.9 — just surface the deadline in `wire tail` output. Future versions
can raise polling cadence or fire louder OS toast near the deadline.
**Files to touch:**
- `src/cli.rs`:
- Add `--deadline <DURATION>` flag to the `Send` clap subcommand.
Accept formats: `30m`, `2h`, `1d`, or an absolute RFC 3339 timestamp.
Pass through to `cmd_send`.
- In `cmd_send`: if `--deadline` was provided, parse it (now + duration
OR explicit RFC 3339) and include `time_sensitive_until: "<iso>"` as
a top-level field on the signed event JSON alongside `from`, `to`,
`kind`, `body`, etc. Field is omitted entirely if no flag passed.
- `cmd_tail` formatting (or wherever `wire tail` renders events):
when `time_sensitive_until` is present on an event, display
`⏰ deadline: <iso>` or `⏰ X min remaining` on the same line as the
event header.
- `src/signing.rs` (or wherever the canonical event-id is computed): if
the canonical-event-id calculation walks event fields, ensure
`time_sensitive_until` is included so signature verification picks up
any tampering of the deadline. Should be automatic if the canonical
form is "all top-level fields excluding signature + event_id sorted
by key" — verify by reading the canonical impl.
- `src/mcp.rs`: add `time_sensitive_until` as an optional param on
`wire_send` MCP tool. Same parsing as the CLI flag.
**Test command:**
```bash
cargo test --release
```
**Acceptance:**
- `wire send peer kind body --deadline 30m` produces a queued event
whose JSON contains a `time_sensitive_until` field exactly 30 minutes
in the future, signed.
- `wire verify <event-with-deadline>.json` still verifies after the
field is added (sig covers the new field).
- `wire tail peer --json` includes the field unchanged on the receiver
side.
- `wire tail peer` (non-JSON) renders the deadline visibly.
- New tests in `tests/cli.rs` or `tests/e2e_bilateral.rs` covering at
least: roundtrip with deadline preserved + verified, no-deadline path
unchanged.
**Non-goals:**
- **DON'T implement receiver-side urgency behavior** (faster polling,
louder toast). That's v0.6 work. R2 phase 1 is just: schema +
send-side flag + tail display.
- Don't add a `--priority` flag (deprecating the field). The existing
`priority` field on events stays unchanged.
- Don't add SLA / deadline-expiry semantics. The field is informational
in v0.5.9.
**Commit message:**
```
[c] R2 v1: time_sensitive_until event field + --deadline CLI flag
From the agent-attention-layer incident (docs/INCIDENT_REPORT_...).
Adds an optional RFC 3339 deadline as a top-level event field; sender
opts in via `wire send --deadline 30m` (or absolute RFC 3339). Receiver
displays the deadline in wire tail output.
Schema is forward-compatible: events without time_sensitive_until parse
unchanged. Sig verification covers the new field via the existing
canonical-form computation.
Deferred to v0.6: receiver-side urgency behavior (faster polling near
deadline, escalated OS toast). v0.5.9 surfaces the deadline; behavior
stays advisory.
- src/cli.rs: --deadline flag + duration/RFC3339 parser + cmd_send
passthrough + tail display
- src/mcp.rs: wire_send MCP tool param parity
- src/signing.rs: confirm canonical-form covers new field (likely no
change needed; verify and document)
- tests: roundtrip with deadline, sig verify, no-deadline path
```
---
## [d] R3: responder-health events + relay slot_state extension
**Why:** From R3. When an operator's auto-responder daemon breaks
(OAuth expired, claude subprocess dead, rate-limited), peers see
"silent" indistinguishably from "healthy but no reply needed." Adding
a responder-health event the operator can emit, surfaced via the
existing `/v1/slot/:slot_id/state` endpoint, gives senders a way to
diagnose before adding more time-sensitive asks.
**Files to touch:**
- `src/relay_server.rs`:
- Extend `Inner` struct with `responder_health: HashMap<String,
ResponderHealthRecord>` keyed by slot_id.
- Define `ResponderHealthRecord { status, reason, last_success_at,
set_at }`. `status` is an enum-ish string: "online", "offline",
"oauth_locked", "rate_limited", "degraded". `reason` is optional
free-text. `last_success_at` + `set_at` are RFC 3339.
- Add `POST /v1/slot/:slot_id/responder-health` handler, auth'd by
slot_token (same bearer pattern as list_events). Body is the
`ResponderHealthRecord` JSON. Stores in-memory in Inner; persists
to `<state_dir>/responder-health/<slot_id>.json` on each set.
- Extend `GET /v1/slot/:slot_id/state` JSON response: include
`responder_health` field with the most recent record or `null` if
none set.
- On startup in `Relay::new`: load any persisted records from disk.
- `src/cli.rs`:
- Add `wire responder set <status> [--reason <text>]` subcommand.
Reads relay-state for own slot_id + slot_token, POSTs the record
to the relay's `/v1/slot/:slot_id/responder-health` endpoint.
- `wire responder get [<peer>]` — read state from relay (own or
peer's), print formatted.
- Both with `--json` flag for machine output.
- `src/relay_client.rs`:
- Add `responder_health_set(slot_id, slot_token, record)` and
`responder_health_get(slot_id, slot_token)` methods (or extend
the existing `slot_state` method to surface the new field — which
it should, automatically, if you keep it on slot_state).
**Test command:**
```bash
cargo test --release --test relay
cargo test --release --test cli
```
**Acceptance:**
- POST /v1/slot/:slot_id/responder-health with a valid bearer + body
returns 200; GET /v1/slot/:slot_id/state then shows the record.
- Wrong bearer returns 403.
- CLI roundtrip: set with `wire responder set offline --reason "OAuth
expired"`, retrieve with `wire responder get`. Matches.
- Persistence: relay restart preserves the most recent record.
- New tests cover both success + auth-fail cases for the endpoint and
CLI roundtrip.
**Non-goals:**
- **Don't auto-emit responder-health events from any existing daemon.**
The CLI command is operator-driven. Auto-emission is v0.6 work.
- Don't define a wire event kind for this (kind=1500 was floated; not
using it for v0.5.9). The endpoint + state are out-of-band relative
to the event stream — meta-information about the slot, not events
flowing through it.
- Don't add a global "responders dashboard" — per-slot only.
**Commit message:**
```
[d] R3: responder-health endpoint + CLI
From the agent-attention-layer incident. When the operator's auto-
responder daemon breaks (OAuth, rate-limit, etc.), peers see "silent"
indistinguishably from "healthy + no reply needed." This commit gives
the operator a way to publish their responder's health and gives
peers a way to read it.
- POST /v1/slot/:slot_id/responder-health — auth'd, persisted, per-slot
- GET /v1/slot/:slot_id/state — extended with responder_health field
- wire responder set <status> [--reason <text>] CLI
- wire responder get [<peer>] CLI
- relay_client: set + get convenience methods
Out of band of the event stream. Slot owner emits via CLI, peer reads
via slot_state. Auto-emission from a daemon is deferred to v0.6.
```
---
## [e] R5: `wire status --peer <handle>` 3-layer health (depends on d)
**Why:** From R5. The incident report calls for distinguishing three
health dimensions: transport (relay reachable), agent attention (last
pull observed), auto-responder (last responder-health event). v0.5.9
already has the data for all three; this task just exposes it via a
single CLI command.
**Files to touch:**
- `src/cli.rs`:
- Extend the existing `wire status` subcommand with a new
`--peer <handle>` flag. When passed, look up the peer in relay-state,
fetch their slot_state from the relay (using the slot_token we hold
from pair), and render three rows:
```
📞 transport ✅ relay reachable (<latency>ms)
👂 attention ✅ last pull 23s ago
(or: ⚠ last pull 18m ago — they may be away
or: ❌ never pulled since our last reset)
🤖 auto-responder ✅ online (last_success: 2026-05-15T20:14Z)
(or: ⚠ degraded — rate_limited
or: ❌ offline — OAuth expired)
(or: — not reported)
```
- Without `--peer`, `wire status` behaves as before (own state).
- `--json` output: structured `{transport: {...}, attention: {...},
responder: {...}}`.
**Test command:**
```bash
cargo test --release --test cli
```
**Acceptance:**
- `wire status --peer <handle>` produces the 3-row output for a paired
peer.
- `wire status --peer <unknown-handle>` errors cleanly.
- `--json` mode parses to the documented schema.
- New tests cover at least: paired peer with no responder-health
reported (responder = "—"), peer with stale attention (>5 min), peer
with offline responder-health.
**Non-goals:**
- Don't probe ALL paired peers at once (`wire status` with no `--peer`
stays scoped to self).
- Don't surface raw slot_id / slot_token in human output.
**Commit message:**
```
[e] R5: wire status --peer <handle> shows 3-layer health
Single CLI surface for transport / attention / auto-responder. Builds
on R4 (slot pull tracking) and R3 (responder-health endpoint). Lets
the operator diagnose "is this peer reachable, awake, and answering?"
in one query.
- src/cli.rs: --peer flag + 3-row formatter + --json schema
- tests: paired peer happy path, no-responder-reported, stale attention
```
---
## [g] Directory endpoint + landing phone-book UI
**Why:** Driven by the Reddit comment thread. Today wire's discovery is
single-handle resolve (`/.well-known/wire/agent?handle=X`). Adding a
listing endpoint `GET /v1/handles` gives a federated phone book:
every relay publishes its own roster, no global registry. Pairs with a
phone-book UI on the landing page.
**Files to touch:**
- `src/relay_server.rs`:
- Add `GET /v1/handles` handler. Query params: `?cursor=<nick>` for
pagination (after-cursor exclusive), `?limit=N` default 100 max 500,
`?vibe=<tag>` optional filter (case-insensitive match against any
element of the profile's `vibe` array).
- Response shape: `{handles: [{nick, did, profile: {emoji, motto,
vibe, pronouns, now}, claimed_at}, ...], next_cursor: <nick> | null}`.
Profile fields are pulled from the stored card; omit private/
operator-only fields.
- Respect a per-handle `listed: false` opt-out: if `card.profile.listed`
is `false`, the handle is excluded from the listing. Default = listed.
- Auth: none — public endpoint. Same posture as `/stats`.
- `src/pair_profile.rs`:
- Add `listed` to `PROFILE_FIELDS` so `wire profile set listed
false` works.
- `landing/index.html`:
- Add a "Now Ringing" / phone-book section that fetches
`/v1/handles?limit=20` on page load, renders each handle as a
one-row entry (emoji, nick, motto, vibe). Group by first vibe tag
if present. Match the existing typewriter / parchment aesthetic.
- Show a "more →" link to a future paginated browser; for v0.5.9
just the first 20.
- JS should be vanilla, no framework. Use `fetch()` + DOM updates.
- Failure mode: if the endpoint 404s or errors, hide the section
silently rather than show a broken state.
**Test command:**
```bash
cargo test --release --test relay
cargo test --release --lib pair_profile
# manual:
cargo run --release --bin wire -- relay-server --bind 127.0.0.1:18770 &
# claim a couple of handles against it, set profiles, then:
curl http://127.0.0.1:18770/v1/handles
curl 'http://127.0.0.1:18770/v1/handles?vibe=nocturnal'
```
**Acceptance:**
- Endpoint returns the correct shape with pagination working
(`cursor=` resumes after the given nick).
- Vibe filter is case-insensitive and matches any element of the array.
- `wire profile set listed false` opts a handle out of the listing.
- Landing fetches + renders without breaking the existing layout.
- New tests cover endpoint + listed-false-opt-out + pagination + vibe
filter.
**Non-goals:**
- Don't add full-text search across motto / now. Only vibe filter for
v0.5.9.
- Don't add a separate `/v1/handles/<nick>` route — the existing
`/.well-known/wire/agent?handle=<nick>` is the single-handle path.
- Don't paginate the landing UI; show first 20 and link "more".
**Commit message:**
```
[g] /v1/handles directory endpoint + landing phone-book UI
Adds a paginated listing of claimed handles on a relay, filterable
by vibe. Federated phone-book pattern — every relay publishes its
own roster, no global registry. Pairs with a "Now Ringing" section
on the landing page that fetches + renders 20 handles.
Privacy: per-handle opt-out via `wire profile set listed false`.
Default is listed=true.
- src/relay_server.rs: GET /v1/handles handler + pagination + vibe
filter + listed opt-out
- src/pair_profile.rs: listed in PROFILE_FIELDS
- landing/index.html: phone-book section, vanilla-JS fetch + render
- tests: pagination, vibe filter, listed-false opt-out
```
---
## [f] `docs/CONSENT_DESIGN.md` — spec-shape design doc
**Why:** Reddit-driven. The cross-machine consent boundary is the
unsolved spec-shape question for agent comms. Wire doesn't solve it
in v0.5.9, but capturing the design space + wire's stance now means
future contributors (and the original Reddit commenter) know what
wire intentionally doesn't do and why.
**This is the one task in this batch that's docs-only and judgment-
heavy.** Codex can draft from the outline below; the operator will
review + edit for tone. Don't try to "solve" consent in this doc —
document the trade-space, name where wire sits, and flag what would
change wire's stance.
**Files to touch:**
- `docs/CONSENT_DESIGN.md` (new) — covers:
- **Problem statement**: MCP assumes consent boundary at the host
(human-in-the-loop). Cross-machine handoff breaks that. Second-hop
agent needs either (a) to ask its human (UX hell), (b) a pre-signed
delegation token, or (c) a receiver-side policy.
- **Three design axes**: transport, identity, consent. Wire is
explicitly the transport layer; identity and consent are separable
concerns.
- **Two consent patterns**:
- Macaroon-style scoped tokens (sender-side: operator pre-signs
`{agent_a may send to agent_b, kind=X, TTL=24h, auto-execute up
to 5/hr}`, token rides in envelope, receiver verifies).
- Receiver-side policy (sender includes `requested_authority` hint,
receiver consults local `policy.json`, decides auto/ask/deny).
- **Wire's v0.5 stance**: receiver-side policy. Why: keeps relay
ciphertext-only (security property worth keeping), keeps the
protocol dumb, doesn't bake consent into the wire envelope.
- **What changes wire's stance**: if cross-org delegation becomes
common-enough to need a uniform format, macaroon path may win.
Trigger: more than one external project asking for it.
- **What v0.5.9 ships**: the `requested_authority` advisory field
on events (sender-side hint only). No receiver-side policy
enforcement yet — that's v0.6.
**Test command:**
None (docs only). But run `cargo build --release` to make sure no
inadvertent code changes leaked into other files.
**Acceptance:**
- File exists at `docs/CONSENT_DESIGN.md`.
- Covers the bullets above.
- Reads as a design doc, not advocacy or marketing.
- Cross-links to `docs/INCIDENT_REPORT_2026_05_12_AGENT_ATTENTION_LAYER.md`
(where the consent question first surfaced as a corollary of R2-R5)
and to the Reddit thread URL if you have it (operator may know).
- Doesn't claim wire solves consent. The doc's job is to name the
problem and document wire's position.
**Non-goals:**
- **Don't implement anything.** This task is the doc only. If task
[c] (R2 `time_sensitive_until`) is already shipped, mention it as
an example of "what protocol-level advisory metadata looks like" —
but don't add a `requested_authority` field as part of this commit.
- Don't survey every consent system ever (UCAN, ZCAP, OAuth scopes,
capabilities). Mention macaroons + receiver-side policy as the two
patterns wire ping-pongs between; keep the rest out.
**Commit message:**
```
[f] docs: CONSENT_DESIGN — wire's stance on cross-machine handoff
Captures the design space surfaced by the Reddit thread on agent-comms
spec gaps. Wire is transport-only by design; consent + identity are
separable concerns. v0.5.x lands on receiver-side policy as the v0.x
direction (keeps relay ciphertext-only, keeps protocol dumb).
Macaroon-style scoped tokens are the alternative we'd ship if cross-
org delegation patterns harden.
This is a design doc, not a roadmap. Captures intent + trade-space,
doesn't bind future versions.
```
---
## [h] Macaroon-style scoped delegation tokens (speculative)
**Why:** Long-term direction from the consent design (task f). NOT
production-bound — this task is research-grade scaffolding so we have
a starting point if v0.6 commits to the macaroon path.
**Caveats:**
- **This is speculative.** Codex should attempt it but if blocked,
ship a partial commit + write up the blocker in `BLOCKED.md` and
move on. Don't sink half a day on it at the expense of [a]-[g].
- **Don't merge into the relay or CLI's main paths.** Implementation
goes in a new `src/macaroon.rs` module + `tests/macaroon.rs`. No
integration with `wire send` or relay handlers yet.
- **Crate choice**: prefer an existing Rust macaroon crate (e.g.,
`libmacaroons-rs` or `branca` for a simpler token format if
macaroon impls are stale). If no clean dep exists, write a minimal
HMAC-chain prototype based on the original macaroon paper (Stanford
2014). The prototype is just to prove the design fits wire's event
envelope.
**Files to touch:**
- `src/macaroon.rs` (new):
- `Macaroon` struct: `{root_key_id, identifier, caveats:
Vec<Caveat>, signature}`.
- `Caveat` enum: `Sender(Did)`, `Recipient(Did)`, `Kind(u32)`,
`Expiry(rfc3339)`, `MaxRate(u32, Duration)`.
- `mint(root_key, identifier, caveats) -> Macaroon`.
- `verify(macaroon, root_key, context) -> Result<()>`. `context`
is what the receiver knows at verify-time (sender did, recipient
did, event kind, current time).
- `serialize` / `deserialize` to/from base64.
- `tests/macaroon.rs` (new):
- mint + verify happy path
- expiry caveat rejects after TTL
- sender caveat rejects mismatched sender
- tampering rejects (modify signature)
- `Cargo.toml`: add dep if using a crate.
- Add a 1-paragraph section to `CONSENT_DESIGN.md` (already created
in task f) pointing at the new module as the speculative
implementation, with a "not used in production yet" disclaimer.
**Test command:**
```bash
cargo test --release --test macaroon
```
**Acceptance:**
- Module compiles. Tests pass.
- No new dependency added without justification (prefer write-it-
ourselves prototype over heavy/abandoned crates).
- CONSENT_DESIGN.md updated with the pointer + disclaimer.
**Non-goals:**
- **Don't wire macaroons into the relay or CLI.** Module is stand-
alone scaffolding.
- Don't ship macaroon-in-envelope on wire events. That's v0.6+.
**If blocked:**
- Add to `BLOCKED.md` at repo root: what you tried, what didn't work,
what info would help. Skip and move on.
**Commit message:**
```
[h] speculative: src/macaroon.rs scaffolding (not production-bound)
Prototype macaroon-style scoped token implementation for future
consent-layer work. Lives in its own module, not wired into the relay
or CLI. Provides mint/verify/serialize + 4 unit tests. Captures the
shape wire would ship if v0.6+ commits to the macaroon path described
in CONSENT_DESIGN.md.
Marked speculative: this is research-grade. The relay still treats
events as ciphertext-only + sigs; consent is still receiver-policy
in v0.5.x.
```
---
## [final] Version bump + CHANGELOG + branch sanity check
**Why:** After all 8 tasks are committed, wrap with a single commit
that bumps the version, fills in the CHANGELOG section, and runs the
final full-suite check.
**Files to touch:**
- `Cargo.toml`: version `0.5.8` → `0.5.9`.
- `Cargo.lock`: regenerate via `cargo build --release` (will update
the wire workspace entry).
- `tests/cli.rs`: version assertion `"0.5.8"` → `"0.5.9"`.
- `CHANGELOG.md`: new section under v0.5 line:
```
### v0.5.9 — directory + R2/R3/R5 + consent design + cleanup
...
```
Pull a short paragraph per task ([a] through [h]) from the commit
messages. Lead with the most operator-visible (R2/R3/R5 + directory),
then the cleanups ([a] [b]), then docs ([f]) + speculative ([h]).
- `README.md`: update the `**Status:**` line to mention v0.5.9 +
highlight the directory + 3-layer health additions.
**Test command:**
```bash
cargo test --release # full suite green
cargo fmt --all --check
cargo clippy --release
cargo run --release --bin wire -- --version # → wire 0.5.9
```
**Acceptance:**
- All three checks pass.
- `wire --version` reports 0.5.9.
- CHANGELOG accurately summarizes the batch.
- Branch is rebased on latest main (in case `main` advanced during
the batch).
**Push:**
```bash
git push -u origin codex/v0.5.9-batch
```
Don't tag v0.5.9 or merge to main. Operator reviews + tags + merges.
**Commit message:**
```
[final] v0.5.9 bump + CHANGELOG + status
Bumps Cargo.toml, tests/cli.rs version assertion, CHANGELOG.md, and
README status line. All 8 task commits ([a] through [h]) on this
branch combine into the v0.5.9 release. Operator reviews the branch
and tags + merges to main.
```
---
## Definition of done for the branch
- 9 commits on `codex/v0.5.9-batch`: one per task [a]-[h] plus [final].
- `cargo test --release` green (modulo the pre-existing
e2e_detached_pair local-only flake; CI verifies clean container).
- `cargo fmt --all --check` clean.
- `cargo clippy --release` no errors.
- `wire --version` → `wire 0.5.9`.
- `CHANGELOG.md` v0.5.9 section is accurate + concise.
- `BLOCKED.md` exists ONLY if at least one task was blocked.
- Branch pushed to origin.
## If you finish early
Run a fresh smoke against a local relay covering:
1. `wire claim alice@local-relay`
2. `wire add bob@local-relay` (set up second home)
3. `wire send bob decision "hi" --deadline 30m` ([c])
4. `wire responder set offline --reason test` ([d])
5. `wire status --peer bob` ([e])
6. `curl http://127.0.0.1:18770/v1/handles` ([g])
7. `curl http://127.0.0.1:18770/.well-known/agent-card.json?handle=alice | jq .extensions[0].uri` ([a])
8. `curl http://127.0.0.1:18770/stats | jq .handle_first_claims_total` ([b])
If any of those return unexpected results, capture the actual vs
expected output in `BLOCKED.md` and report back. Don't try to fix
forward without confirming the design.
## Out of scope for this batch
These were considered and explicitly deferred:
- **Wire-client default-relay update** (followup queued in
SESSION_LOG_2026_05_12). Already done since v0.5.2 (`DEFAULT_RELAY
= "https://wireup.net"`). No-op.
- **CF Pages project cleanup** (operator-side, CF dashboard click).
- **Spark wire-public-relay-state pruning** (operator-side, calendar
task).
- **R2 receiver-side urgency behavior** (deferred to v0.6).
- **R3 daemon auto-emission of responder-health** (deferred to v0.6).
- **Wire `--require-sas` flag for opt-back-into-SPAKE2** — already
exists from earlier work.
- **Federation-spec proper RFC for the wire A2A extension** — wait
until external A2A consumers exist.
If Codex sees a tempting refactor that touches more than one task's
files, **don't do it.** Atomic commits per task is the goal. Refactor
in a separate v0.5.10 if it's genuinely useful.