Skip to content

Commit 06221af

Browse files
committed
docs(nostr): design dossier for the Nostr mobile agent bridge
PRD-017 + ADR-092..097 + DDD context + the eight integration-research notes specifying an Android Nostr client/signer, the mobile-bridge messaging substrate and relay topology, admin-pubkey permission and delegation, the session-summary event scheme, and the Solid-pod persistence boundary. Design only; no runtime changes. Co-Authored-By: jjohare <github@thedreamlab.uk>
1 parent 07ebd2e commit 06221af

16 files changed

Lines changed: 5177 additions & 0 deletions

docs/PRD-017-nostr-mobile-agent-bridge.md

Lines changed: 227 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# ADR-092 — Android Nostr Client and Signer for the Mobile Agent Bridge
2+
3+
| Field | Value |
4+
|-------|-------|
5+
| Status | Proposed (2026-06-02) |
6+
| Drives | PRD-017 §5.1 (F1-F4) |
7+
| Companion ADRs | ADR-093 (messaging substrate), ADR-094 (permissioning), ADR-096 (pod boundary) |
8+
| Affected repos | `agentbox` (relay exposure, consumer); operator device (client install) |
9+
| Evidence | `docs/integration-research/nostr-mobile-bridge/06-android-client-landscape.md` |
10+
11+
## Context
12+
13+
The mobile bridge needs an Android Nostr client to be the operator's interactive surface into agentbox agents. The client must support encrypted DMs (NIP-17), relay AUTH (NIP-42), and — critically — must not require the operator to paste their nsec into an app process that also renders untrusted feed content. The ecosystem is `did:nostr`-keyed; the client is a Nostr citizen, not a Solid citizen (research 05 §6 establishes that the pod-write boundary is on the agent side, so the client has no Solid requirement).
14+
15+
13 Android clients were surveyed (research 06). The discriminators that matter: full NIP-17/44/59 support (not legacy NIP-04), NIP-42 AUTH (the agentbox and CF relays both challenge on connect), and NIP-55 external-signer support (key isolation).
16+
17+
## Decision
18+
19+
### D1 — Primary stack: Amethyst + Amber
20+
21+
Adopt **Amethyst** as the recommended Android client and **Amber** as the NIP-55 external signer. Amethyst has the most complete NIP-17/44/59 implementation of any Android client and native NIP-55 signer delegation. Amber holds the private key in a separate process and signs via Android `Intent`; the key never enters Amethyst's address space.
22+
23+
### D2 — Key isolation via NIP-55, not NIP-46
24+
25+
Use NIP-55 (on-device Android signer intent) for key isolation. Do NOT implement NIP-46 (remote bunker). NIP-46 is absent from all four ecosystem codebases (research 04 §6.3) and would be a from-scratch build with a per-request relay round-trip on the latency path. NIP-55 gives equivalent custody isolation (the key lives in Amber, not the client) with zero new server-side code.
26+
27+
### D3 — Fallback stack: 0xchat + Amber
28+
29+
If a DM-focused surface is preferred over Amethyst's broad social feed, **0xchat** is the sanctioned fallback. It is DM-first with strong NIP-17 support and uses the same Amber signer, so D2 holds unchanged. Coracle (PWA) is a documented browser-based alternative but is not the recommendation (no native NIP-55).
30+
31+
### D4 — No bespoke Android development
32+
33+
We adopt an existing client. No custom Android app is built. This bounds scope to relay/consumer/agent-side work.
34+
35+
## Consequences
36+
37+
**Positive:**
38+
- Key custody is isolated from feed-rendering code (NIP-55).
39+
- Client choice is fully decoupled from Solid (research 05 §6) — no special client capability needed.
40+
- Zero Android engineering; all build effort is server-side (PRD-017 §8 Phase 1 items).
41+
42+
**Negative / risks:**
43+
- Amethyst renders a broad social feed; for a focused control channel this is noise. Mitigated by D3 (0xchat fallback) or by using Amethyst's DM view exclusively.
44+
- We depend on third-party client NIP-44 v2 conformance. This is why PRD-017 BLOCK-1 mandates a live Amethyst+Amber round-trip against the NIP-44 v2 test vectors before ship.
45+
- Amber must be installed and the delegation key provisioned into it (ADR-094 D-series covers the delegation; the operator imports the delegated key into Amber).
46+
47+
## Alternatives considered
48+
49+
- **Phone holds the raw admin nsec in the client** (research 04 §7.1, Option A): rejected — single point of failure for the whole agentbox identity; key loss forces full identity rotation including pod ACLs and DID docs.
50+
- **NIP-46 bunker on the phone** (Option C): rejected per D2 — no ecosystem code, latency cost, not on any roadmap.
51+
- **Damus/Primal/Nostros etc.**: surveyed (research 06); weaker NIP-17 or no NIP-55. Not selected.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ADR-093 — Mobile Bridge Messaging Substrate (NIP-17 / NIP-44 / NIP-59)
2+
3+
| Field | Value |
4+
|-------|-------|
5+
| Status | Proposed (2026-06-02) |
6+
| Drives | PRD-017 §5.2 (F5-F9), §5.5 (F20-F22) |
7+
| Companion ADRs | ADR-092 (client), ADR-094 (permissioning), ADR-095 (summaries), ADR-097 (topology) |
8+
| Affected repos | `agentbox` (relay-consumer call-site, outbound publisher); `nostr-rust-forum` (`nostr-bbs-core` crypto crate — consumed via WASM, one `wasm_bridge.rs` shim) |
9+
| Evidence | `docs/integration-research/nostr-mobile-bridge/07-nip-substrate.md`, `03-cf-private-relay.md`; `nostr-bbs-core/{nip44,gift_wrap,nip26}.rs`, `tests/e2e_auth_flow.rs` |
10+
11+
## Context
12+
13+
The bridge needs a wire protocol for phone↔agent messaging. The candidates are NIP-04 (legacy DM, kind 4), NIP-17 (private DM, kinds 14/15), NIP-28/29 (public/group chat), and NIP-90 (DVM structured jobs). The choice must give per-recipient confidentiality, metadata privacy, and an interactive surface, while remaining forward-compatible with the CF private relay and an eventual admin control panel.
14+
15+
Two facts constrain the choice:
16+
- The CF relay gives kind-4 (NIP-04) NO per-recipient isolation — it is served to any subscriber (research 03 §4). Only kind-1059 (NIP-59 gift wrap) gets AUTH + `#p`-recipient isolation (research 03 §3, `nip_handlers.rs:398-430`).
17+
- The agentbox embedded relay stores kind-1059 but never decrypts it (ADR-009:262 deferral). The decryption *primitives* are NOT net-new: `nostr-bbs-core` already implements and e2e-tests the full NIP-59 gift-wrap roundtrip (`gift_wrap.rs` `unwrap_gift`/`seal_rumor`/`wrap_seal`) over NIP-44 v2 (`nip44.rs`), and the forum ships them to JS via `wasm_bridge.rs`. What is net-new is only the agentbox **call site** that invokes the existing decrypt at the dispatch boundary.
18+
19+
## Decision
20+
21+
### D1 — NIP-17 is the primary chat transport
22+
23+
Ad-hoc chat uses **NIP-17**: kind-14 chat / kind-15 file rumors, sealed in kind-13, gift-wrapped in kind-1059 per **NIP-59**, encrypted with **NIP-44 v2** (ChaCha20 + HMAC-SHA256, HKDF-derived keys). This is the only DM scheme that gives both content confidentiality and sender-metadata privacy, and it is the kind that the CF relay isolates per-recipient. NIP-17 is the MVP path.
24+
25+
### D2 — NIP-04 is forbidden
26+
27+
Kind-4 NIP-04 DMs MUST NOT be used. No per-recipient isolation on the CF relay (research 03 §4), leaks metadata, and is deprecated upstream. The agentbox consumer MUST NOT fall back to NIP-04.
28+
29+
### D3 — NIP-90 for structured tasks (optional, Phase 1)
30+
31+
When the operator wants a typed job rather than free chat, use **NIP-90 DVM** kinds: 5xxx request, 6xxx result, 7000 feedback. This is additive over the chat path and optional in Phase 1; the chat path (D1) is the floor.
32+
33+
### D4 — ACSP control panels for admin actions (Phase 2)
34+
35+
Structured admin control (e.g. the synchronous approve/reject that CTM does with inline buttons) uses **ACSP** over NIP-33 addressable kinds 31400-31405 (PanelDefinition/State/ActionRequest/Response/PanelUpdate/PanelRetired). Deferred to Phase 2; these kinds are already agent-registry-gated on the CF relay (research 03 §4, `nip_handlers.rs:221-232`).
36+
37+
### D5 — Outbound mirroring is NIP-17
38+
39+
Agent→phone turn output, session start/end, and (optional, verbose) tool activity are delivered as NIP-17 DMs to the phone pubkey (PRD-017 F20-F22). One logical conversation per session.
40+
41+
### D6 — Consume the existing decryption path; wire the call-site in the relay-consumer
42+
43+
The agentbox relay-consumer MUST gain a kind-1059 unwrap path: unwrap gift wrap → unseal kind-13 → recover kind-14 rumor → NIP-44 v2 decrypt. This is **not** a from-scratch build. `nostr-bbs-core` already implements every step (`gift_wrap.rs` `unwrap_gift:307`, `seal_rumor:179`, `wrap_seal:226`; `nip44.rs` `decrypt:81`, `conversation_key:97`), benched and roundtrip-tested (`e2e_auth_flow.rs:8` "Gift wrap (NIP-59): encrypt → wrap → unwrap → decrypt for DM relay routing"). The crate is already first-party built and run as a service in agentbox; the forum already consumes the same crate from JS via WASM.
44+
45+
The residual work is integration: (1) consume `nostr-bbs-core` from the relay-consumer — either through the existing `wasm_bridge.rs` exports or a Rust path; (2) add the one missing `#[wasm_bindgen]` shim re-exporting `gift_wrap`/`unwrap_gift` (the impl is `pub` in `gift_wrap.rs` but `wasm_bridge.rs` currently exposes only `nip44_encrypt`/`nip44_decrypt`, schnorr, and NIP-98); (3) invoke it at the dispatch boundary. NIP-44 conformance (PRD-017 BLOCK-1) is satisfied by the existing implementation plus a live Amethyst/Amber interop check — not by writing crypto.
46+
47+
## Consequences
48+
49+
**Positive:**
50+
- Single confidential transport that the CF relay already isolates per-recipient — Phase 2 federation needs no DM-scheme change.
51+
- Layered design: chat (D1) ships first; structured tasks (D3) and admin panels (D4) layer in without reworking transport.
52+
53+
**Negative / risks:**
54+
- NIP-44 v2 correctness is load-bearing and unobservable on the happy path — but it is already implemented, benched, and e2e-tested in `nostr-bbs-core`; BLOCK-1 reduces to consuming it and proving live Amethyst/Amber interop against the published vectors.
55+
- The cryptographic risk is concentrated in a single, already-tested crate rather than spread across a new agentbox implementation — a consequence of consuming `nostr-bbs-core` rather than hand-rolling. The remaining integration risk is at the call-site (correct argument marshalling, error handling) and the WASM boundary.
56+
- Cross-repo coupling: agentbox now depends on `nostr-bbs-core` as a shared library. This is library reuse, not runtime forum interop — the crate is already the shared substrate under the relay worker, auth worker, and forum client, so the PRD-017 "no forum AT THIS TIME" constraint holds.
57+
- The agentbox embedded relay must match the CF relay's kind-1059 `#p`-isolation semantics, or recipient isolation (NFR-3) degrades in Phase 1.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# ADR-094 — Admin-Pubkey Permission Model and NIP-26 Phone Delegation
2+
3+
| Field | Value |
4+
|-------|-------|
5+
| Status | Proposed (2026-06-02) |
6+
| Drives | PRD-017 §5.3 (F12-F13), §7.1 (BLOCK-3) |
7+
| Companion ADRs | ADR-092 (client/signer), ADR-093 (substrate), ADR-074 (did:nostr canonicalisation) |
8+
| Affected repos | `agentbox` (relay-consumer auth, management-api auth.js), `nostr-rust-forum` (`nip26.rs` source) |
9+
| Evidence | `docs/integration-research/nostr-mobile-bridge/04-did-nostr-identity.md`, `01-telegram-bridge-teardown.md` |
10+
11+
## Context
12+
13+
The bridge's control channel is the operator's most privileged surface — a message that reaches agent dispatch can make the agent act. Authorisation must be `did:nostr`-native (not Telegram-style chat membership) and must not require the operator's phone to hold the agentbox's own identity key.
14+
15+
Current state:
16+
- agentbox management-api runs `strict-nip98` when sovereign mesh is enabled (`auth.js:105-108`); admin routes gate on `admin_pubkeys` (`agentbox.toml:159`) or the operator pubkey `AGENTBOX_X_ONLY_PUBKEY_HEX`.
17+
- NIP-26 delegation is fully implemented (`nostr-bbs-core/src/nip26.rs`: `DelegationToken::create:150`, `verify:178`, `validate_delegation_tag:258`) but NOT wired into any consumer ingress path (research 04 §4.2) — including the agentbox relay-consumer `_processEvent`.
18+
- CTM's `IdentityStore` maps `did:nostr:<pubkey>` → role but `is_allowed()` is never called in the message path (research 01 §6).
19+
20+
## Decision
21+
22+
### D1 — Authorisation is admin-pubkey gated, signature-first
23+
24+
An inbound message is dispatched to an agent only if, AFTER Schnorr signature verification, the unwrapped rumor's author pubkey is one of:
25+
1. `AGENTBOX_X_ONLY_PUBKEY_HEX` (the operator/agent identity), or
26+
2. an entry in `agentbox.toml [sovereign_mesh.multi_user] admin_pubkeys`, or
27+
3. a key bearing a valid NIP-26 delegation (D2) from (1) or (2).
28+
29+
The check runs at the relay-consumer dispatch boundary, before any agent side effect. Signature verification precedes authorisation — an unsigned or forged event is rejected before its pubkey is even consulted (NFR-2).
30+
31+
### D2 — Phone holds a NIP-26 delegated key (not the admin nsec)
32+
33+
The phone holds its OWN independent secp256k1 keypair with its own `did:nostr`. The admin key signs a NIP-26 delegation token authorising it:
34+
35+
```
36+
conditions: kind=14&kind=1059&created_at>T_start&created_at<T_end
37+
tag: ["delegation", "<admin_pubkey_hex>", "<conditions>", "<schnorr_sig>"]
38+
```
39+
40+
The phone appends this delegation tag to its events. The admin nsec stays on the agentbox; it is never imported into the phone or Amber. This satisfies PRD-017 G4 and preserves the agentbox container-key stability invariant (DDD A-Inv-04).
41+
42+
### D3 — Wire the EXISTING delegation validator into the relay-consumer
43+
44+
`validate_delegation_tag` (`nostr-bbs-core/nip26.rs:258`) already implements the full check and is unit-tested: parse the delegation tag, verify the delegator's Schnorr signature over `SHA-256("nostr:delegation:" || delegatee_pubkey_hex || ":" || conditions)` (`nip26.rs:281`), check conditions permit the event kind and timestamp. The net-new work is to **call it** in the agentbox relay-consumer dispatch path and confirm the delegator is in the admin set (D1) — wiring, not cryptography. This is PRD-017 BLOCK-3 — without invoking the existing verifier, the delegation is trusted blind and the whole model collapses; the risk is a missing call, not an unimplemented algorithm.
45+
46+
### D4 — Revocation is window-bounded
47+
48+
NIP-26 has no explicit revocation. Delegation windows MUST be short (operator-tunable; recommend ≤ 7 days) and reissued by an agentbox-side tool. On phone compromise, the immediate mitigation is to stop reissuing and let the window expire; a forced kill is achieved by issuing a token whose window is already closed (research 04 §7.2). For faster revocation, the admin may remove the delegated pubkey from any consumer-side allow-cache.
49+
50+
### D5 — Management-api parity
51+
52+
When the phone calls the management-api (NIP-98, kind-27235), the same delegation acceptance applies: `auth.js` MUST, after `verifyNip98Header` succeeds, detect a delegation tag, validate it (D3), and treat the event as admin if the delegator is in the admin set. The delegation conditions for management-api use restrict to `kind=27235`.
53+
54+
## Consequences
55+
56+
**Positive:**
57+
- `did:nostr`-native auth, consistent with every other ecosystem surface; reuses the existing, test-covered `nip26.rs`.
58+
- Admin identity key never leaves the agentbox; phone is a revocable, first-class delegated identity.
59+
- Closes the long-standing gap where `IdentityStore`/delegation existed but were never enforced (research 01 §6, research 04 §4.2).
60+
61+
**Negative / risks:**
62+
- BLOCK-3 is wiring, not crypto: `validate_delegation_tag` is implemented and unit-tested in `nostr-bbs-core`. The security-critical risk is a *missing or mis-ordered call* — trusting the delegation tag without invoking the existing verifier is a full auth bypass. Covered by the 15-row AUTH matrix + 12 negative tests (research 08).
63+
- Window-bounded revocation means a compromised phone retains access until window expiry unless the consumer-side cache is updated. D4 provides the faster path.
64+
- Delegation reissue is operational overhead; needs an agentbox-side reissue tool.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ADR-095 — Session-as-Summary Event Scheme (kind-30840)
2+
3+
| Field | Value |
4+
|-------|-------|
5+
| Status | Proposed (2026-06-02) |
6+
| Drives | PRD-017 §5.4 (F15-F19) |
7+
| Companion ADRs | ADR-093 (substrate), ADR-096 (pod boundary), ADR-097 (topology) |
8+
| Affected repos | `agentbox` (summary generator, dual writer) |
9+
| Evidence | `docs/integration-research/nostr-mobile-bridge/07-nip-substrate.md`, `01-telegram-bridge-teardown.md`, `05-solid-pod-interaction.md` |
10+
11+
## Context
12+
13+
The user wants to "manage sessions via summaries generated about the work done." CTM does NOT generate session summaries — it only forwards Claude Code's own `transcript_summary`/`last_assistant_message` from `Stop` hooks (research 01 §5, `hook.rs:360-393`). The raw material (transcript JSONL, per-turn summary, tool records) exists but no summary artifact is produced. This must be built.
14+
15+
A summary needs two homes: a Nostr event the phone can read in any client (live, browsable feed), and a durable user-owned record (the pod, per ADR-096). The Nostr representation must be addressable so re-publishing updates the same logical session record rather than appending duplicates.
16+
17+
## Decision
18+
19+
### D1 — Session summaries are kind-30840 addressable events
20+
21+
Each session is represented by one **kind-30840** event (NIP-33 parameterised-replaceable; `30000-39999` range → `d`-tag dedup, research 03 §2). The `d` tag is the session id, so re-publishing (status transitions active→complete) replaces in place. Schema:
22+
23+
```jsonc
24+
{
25+
"kind": 30840,
26+
"pubkey": "<agent_pubkey>",
27+
"tags": [
28+
["d", "<session_id>"],
29+
["p", "<admin_pubkey>"], ["p", "<phone_pubkey>"],
30+
["agent", "<agent_pubkey>"],
31+
["relay", "<wss://relay_url>"],
32+
["start", "<unix_ts>"], ["end", "<unix_ts>"],
33+
["status", "active|complete|failed"],
34+
["t", "session-summary"],
35+
["alt", "Agent session summary"]
36+
],
37+
"content": "{\"title\":...,\"summary\":...,\"tool_calls\":<int>,\"tokens_used\":<int>,\"outcome\":...}"
38+
}
39+
```
40+
41+
The `content` is a JSON object (not encrypted in Phase 1 — the summary is intended to be operator-readable in any client; if confidentiality is required later, wrap in NIP-44). The `p` tags address both operator and phone so the phone's client surfaces it.
42+
43+
### D2 — Dual write: relay event AND pod resource
44+
45+
On session end (and at optional checkpoints), the agent MUST both:
46+
1. publish the kind-30840 event to the relay (D1), and
47+
2. write a JSON-LD summary to the operator's pod at `/sessions/<iso-date>-<session-id>.jsonld` (ADR-096).
48+
49+
The relay event is the live/browsable view AND a durable record — the CF relay is a Cloudflare Worker + Durable Object (`nostr-bbs-relay-worker/src/relay_do/`), so kind-30840 persists transactionally there. The pod resource is therefore not the *only* durable copy; its distinct value is **self-sovereign ownership and export** — the operator owns and controls the pod, whereas the relay (however durable) is operator-controlled but not the user's sovereign store. They carry the same logical content; the pod resource additionally carries `owner_did`, `action_urn`, and resource URNs (research 05 §7).
50+
51+
### D3 — Summary generation source
52+
53+
Phase 1 generates `content` from Claude Code's own `transcript_summary` (`Stop` hook field) — present today, zero token cost. A richer dedicated LLM summarisation pass over the transcript/tool record is an OPTIONAL upgrade (research 01 §5 notes both the raw material and CTM's optional Haiku summariser pattern, `summarizer.rs:144`). The choice is a PRD-017 open question; D3 fixes the Phase 1 default as `transcript_summary`.
54+
55+
### D4 — kind allocation reservation
56+
57+
kind-30840 is reserved for session summaries across the ecosystem. It sits outside the forum's used ranges (kind-0/3/1/4/7/40-42/1059/1984/30910-30916/31400-31405 per research 03 §1) and the ACSP governance band (31400-31405). Phase 3 forum interop must not reuse 30840 for another purpose.
58+
59+
## Consequences
60+
61+
**Positive:**
62+
- Delivers the "manage via summaries" capability CTM lacks, in a form any Nostr client can render.
63+
- Addressable (`d`-tag) means status transitions update one record — no duplicate session entries in the phone feed.
64+
- Dual write reconciles "browsable now" with "durable + sovereign" (ADR-096).
65+
66+
**Negative / risks:**
67+
- Dual write is two operations that can partially fail; the agent must handle "event published, pod write failed" (and vice versa) without losing the summary. Idempotency via `d`-tag (relay) and deterministic path (pod) makes retry safe.
68+
- Unencrypted `content` (D1) means any relay subscriber with the filter can read summary text. Acceptable for Phase 1 on the private agentbox relay (whitelist-gated writes; summaries are operator-addressed); revisit before CF-relay federation if summaries are sensitive.

0 commit comments

Comments
 (0)