Files

186 lines
14 KiB
Markdown

---
phase: 13-aiui-functional-conversational-node-control-and-content-surf
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- core/archipelago/examples/routstr_probe.rs
- .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/COVERAGE.md
- .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-ROUTSTR-FINDINGS.md
autonomous: true
requirements: [AIUI-01]
must_haves:
truths:
- "Routstr's wire contract is recorded from a live observation or its unavailability is recorded — the Routstr client in 13-13 is never written against docs alone (RESEARCH Open Question 3)"
- "COVERAGE.md's three `INTEGRATE — UNCONFIRMED` rows are either confirmed against a live provider or explicitly downgraded with a reason"
- "The probe is an `examples/` binary, not a shipped code path — nothing in this plan changes the archipelago daemon"
artifacts:
- path: "core/archipelago/examples/routstr_probe.rs"
provides: "Live Nostr kind-38421 subscribe + provider capability probe, run by hand"
contains: "38421"
- path: ".planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-ROUTSTR-FINDINGS.md"
provides: "Observed event shape, header spelling, arguments encoding, price/model fields — or a recorded no-provider-found"
key_links:
- from: "core/archipelago/examples/routstr_probe.rs"
to: "core/archipelago/src/nostr_discovery.rs"
via: "reuses the Tor-proxy-aware build_nostr_client pattern rather than constructing a second client"
pattern: "build_nostr_client|Client::new"
---
<objective>
Answer RESEARCH Open Question 3 before it becomes a rewrite. `13-RESEARCH.md` rates the Routstr
protocol **MEDIUM** confidence — every claim about kind `38421`, the `Authorization: Bearer
cashuA…` vs `X-Cashu:` header spelling, and OpenAI-compat's JSON-string-encoded
`tool_calls[].function.arguments` is cited from `docs.routstr.com` and has never been run
against a live provider. `13-PATTERNS.md` records "no analog — first OpenAI-compatible client
in this codebase."
Writing `backends/routstr.rs` (13-13) against docs alone is how a young, actively-developed
external project turns into a debugging session inside a security-sensitive agent loop.
Purpose: a cheap, early, throwaway-safe probe that either confirms the contract or records
honestly that no live provider was reachable — so 13-13 starts from a fact, and COVERAGE.md
stops carrying three unconfirmed rows.
Output: an `examples/` probe binary, `13-ROUTSTR-FINDINGS.md`, and a rewritten COVERAGE.md.
</objective>
<flagged_assumptions>
None in this plan.
</flagged_assumptions>
<artifacts_this_phase_produces>
Symbols created by **this plan**:
- `core/archipelago/examples/routstr_probe.rs`: `fn main`, `async fn discover_providers`,
`async fn probe_capabilities`, `const ROUTSTR_KIND: u16 = 38421`, `const DEFAULT_RELAYS`
- New file `.planning/phases/13-.../13-ROUTSTR-FINDINGS.md`
No daemon source file, no `Cargo.toml` dependency, and no RPC method is added by this plan.
</artifacts_this_phase_produces>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-RESEARCH.md
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/COVERAGE.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Probe a live Routstr provider over Nostr and HTTP</name>
<files>core/archipelago/examples/routstr_probe.rs</files>
<read_first>
- `core/archipelago/src/nostr_discovery.rs``build_nostr_client` and how this codebase subscribes with a filter through the Tor proxy. **Reuse this shape; do not construct a second, un-Tor-aware nostr-sdk client.**
- `core/archipelago/Cargo.toml` lines 83-90 — `reqwest` 0.11 (`json`,`socks`,`rustls-tls`,`stream`) and `nostr-sdk` 0.44 (`nip04`,`nip44`) are already present. An `examples/` target links the package's dependencies, so **no `Cargo.toml` change is needed and none may be made.**
- `.planning/phases/13-.../13-RESEARCH.md` "Routstr chat-completions call shape" and the "Sources / Secondary (MEDIUM confidence)" block — the exact claims under test.
- `.planning/phases/13-.../COVERAGE.md` — the three rows marked `INTEGRATE — UNCONFIRMED` are this probe's checklist.
</read_first>
<action>
Create `core/archipelago/examples/routstr_probe.rs` — a standalone throwaway probe, run by hand with `cd core && cargo run --example routstr_probe`. It is an example, not a test and not a daemon path: nothing it does is shipped.
`discover_providers` subscribes to the relays cited in RESEARCH (`wss://relay.damus.io`, `wss://relay.nostr.band`, `wss://nos.lol`) with a filter on kind `38421`, waits up to 30 seconds, and prints every matching event verbatim: full tag list, full content, pubkey, created_at. Do not parse into a typed struct — the whole point is to see what is actually published rather than what a struct expects. Also run a second subscription with **no** kind filter but a `#d` tag filter on `routstr-provider`, in case the kind number in the docs has drifted; print anything it finds.
`probe_capabilities` takes the first discovered provider endpoint (or a `--endpoint` argv override so the probe is still useful when discovery finds nothing) and issues three unauthenticated `GET`s — `/v1/models`, `/`, and the provider's advertised info path if one appears in the event — printing status code and body for each. It must NOT send a Cashu token: this probe spends no money. If a `402` or a `401` body describes the expected payment header, print that body verbatim — that response is the single most valuable artifact this probe can capture, because it is the provider naming its own header spelling.
Print a final summary block answering exactly five questions in plain text: (1) was a live kind-38421 event observed? (2) what are its tag names and content keys? (3) what field carries the model list and what field carries the price? (4) what payment header does the provider name in a 401/402 body? (5) does `/v1/models` respond, and does its shape match OpenAI's?
Handle "no provider found" as a first-class outcome, not an error: print `NO LIVE PROVIDER OBSERVED` and exit 0. A probe that panics when the ecosystem is quiet teaches nothing.
</action>
<verify>
<automated>cd core &amp;&amp; CARGO_INCREMENTAL=0 cargo build --example routstr_probe 2>&amp;1 | tail -5</automated>
<automated>cd core &amp;&amp; timeout 180 cargo run --example routstr_probe 2>&amp;1 | tail -40</automated>
<automated>cd core &amp;&amp; git diff --exit-code -- archipelago/Cargo.toml</automated>
</verify>
<acceptance_criteria>
- `cd core && cargo build --example routstr_probe` exits 0
- `grep -q "38421" core/archipelago/examples/routstr_probe.rs`
- `grep -c "cashu" core/archipelago/examples/routstr_probe.rs` may be > 0 only in printed/parsing code — `grep -ci 'build_payment_token\|auto_pay_token' core/archipelago/examples/routstr_probe.rs` returns 0 (the probe spends nothing)
- `cd core && git diff --exit-code -- archipelago/Cargo.toml` exits 0 — no dependency was added
- `cargo run --example routstr_probe` exits 0 and its output ends with a summary block that either answers all five questions or states `NO LIVE PROVIDER OBSERVED`
</acceptance_criteria>
<reversibility rating="reversible">An `examples/` file is deletable at any time and links no shipped code.</reversibility>
<done>The probe builds, runs to completion, spends nothing, and prints either a live event's real shape or an explicit no-provider-observed result.</done>
</task>
<task type="auto">
<name>Task 2: Record the findings and rewrite the coverage matrix from them</name>
<files>.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-ROUTSTR-FINDINGS.md, .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/COVERAGE.md</files>
<read_first>
- `.planning/phases/13-.../COVERAGE.md` (full file) — specifically the three `INTEGRATE — UNCONFIRMED` rows and the closing `## Gate` section, which names this task as the gate 13-13 waits on.
- The raw probe output from Task 1.
- `.planning/phases/13-.../13-RESEARCH.md` Assumptions Log entry **A2**, which is the assumption this task retires or upholds.
</read_first>
<action>
Write `13-ROUTSTR-FINDINGS.md` containing the probe's verbatim output (trimmed to the relevant events and bodies), the date and the relay set used, and a short table with one row per RESEARCH claim under test: the claim as cited, the observed value, and a verdict of `CONFIRMED`, `DIFFERS` (with the real value) or `NOT OBSERVED`. Cover at minimum: event kind number, `d` tag value, the content keys carrying `endpoints`/`models`/`pricing`, the payment header spelling, and whether `tool_calls[].function.arguments` arrives as a JSON-encoded string.
Then rewrite `COVERAGE.md`'s matrix from those findings, not from the docs:
- Every row that the probe confirmed loses its `— UNCONFIRMED` suffix.
- Every row the probe found to differ is corrected to the observed reality.
- Every row the probe could not observe is downgraded to `OPT-OUT` with the one-line reason `not observable — no live provider reachable on <date>`, or kept as `INTEGRATE` **only** if 13-13's first task is changed to a `checkpoint:decision`. Say which, explicitly, in the `## Gate` section.
- Do not leave a row marked `INTEGRATE` on confidence this plan did not obtain. An opt-out without a reason, or an integrate without evidence, is exactly the un-decided hole the coverage gate exists to close.
Update RESEARCH assumption **A2**'s risk line in `13-ROUTSTR-FINDINGS.md` (not by editing RESEARCH.md) to state whether A2 held.
Commit both files with `docs(13): routstr protocol findings + coverage matrix from live probe` and push per CLAUDE.md.
</action>
<!-- planner-discipline-allow: UNCONFIRMED -->
<verify>
<automated>test -f .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-ROUTSTR-FINDINGS.md</automated>
<automated>grep -c 'UNCONFIRMED' .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/COVERAGE.md | grep -qx 0</automated>
<automated>awk -F'|' '/OPT-OUT/ {if (length($4) &lt; 12) {print "MISSING REASON:" $0; exit 1}}' .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/COVERAGE.md</automated>
</verify>
<acceptance_criteria>
- `13-ROUTSTR-FINDINGS.md` exists and contains a verdict table where every row's verdict is one of `CONFIRMED`, `DIFFERS` or `NOT OBSERVED`
- `grep -c 'UNCONFIRMED' COVERAGE.md` returns 0 — every row now carries either evidence or an explicit downgrade
- Every `OPT-OUT` row in COVERAGE.md has a non-empty reason cell (the `awk` gate above exits 0)
- COVERAGE.md's `## Gate` section states in one sentence whether 13-13 may proceed directly or must open with a `checkpoint:decision`
- Both files are committed and pushed
</acceptance_criteria>
<done>COVERAGE.md contains zero unconfirmed integrations and zero reasonless opt-outs, and 13-13's entry condition is stated as a fact rather than a hope.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| dev workstation → public Nostr relays | Outbound WebSocket; relay operators see the subscription |
| dev workstation → an unknown third-party Routstr endpoint | Outbound HTTP to an endpoint discovered from an untrusted, self-published Nostr event |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-13-16 | Spoofing | A hostile actor publishes a fake kind-38421 event advertising a malicious endpoint | medium | mitigate | The probe treats every discovered endpoint as untrusted data: it only issues unauthenticated GETs, sends no token, no key and no node identity, and prints rather than parses. Provider *trust* selection is 13-13's problem, gated by D-05's budget cap |
| T-13-17 | Denial of Service (financial) | Probe accidentally spends ecash | low | mitigate | The probe never calls `auto_pay_token`/`build_payment_token`; asserted by an acceptance grep. No wallet code is linked into the example's call graph |
| T-13-18 | Information Disclosure | Probe leaks node identity to relays or providers | low | mitigate | Run from a dev workstation, not a node; the probe generates an ephemeral key for the subscription and sends no node-identifying header |
| T-13-19 | Tampering | Findings recorded from docs rather than observation, defeating the plan's purpose | medium | mitigate | `13-ROUTSTR-FINDINGS.md` must carry verbatim probe output; the verdict vocabulary forces `NOT OBSERVED` rather than an optimistic `CONFIRMED` |
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | **Zero** packages added — asserted by `git diff --exit-code -- archipelago/Cargo.toml`. No install task, so no legitimacy checkpoint is required |
</threat_model>
<verification>
- `cd core && cargo build --example routstr_probe` exits 0
- `cd core && git diff --exit-code -- archipelago/Cargo.toml` exits 0
- `grep -c UNCONFIRMED COVERAGE.md` == 0
- `13-ROUTSTR-FINDINGS.md` exists with a verdict per RESEARCH claim
</verification>
<success_criteria>
13-13 can be executed against an observed protocol or an explicitly recorded absence, and
COVERAGE.md is a subtraction record backed by evidence rather than by documentation.
</success_criteria>
<output>
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-03-SUMMARY.md` when done
</output>