feat(http-api): request-id'd /notes/~/v1 + OpenAPI/AsyncAPI specs + X-Api-Key auth + every flow through one dispatch#5
Open
arthyn wants to merge 8 commits into
Open
feat(http-api): request-id'd /notes/~/v1 + OpenAPI/AsyncAPI specs + X-Api-Key auth + every flow through one dispatch#5arthyn wants to merge 8 commits into
arthyn wants to merge 8 commits into
Conversation
Wires a venter-style request/response surface alongside the existing %notes-action / %notes-command path. Each action carries a request-id (@UV); the host emits a response-update on a per-request path; the subscriber forwards it as a response and resolves the held HTTP request (or per-request SSE subscriber). Modeled on tloncorp/tlon-apps PR 5334 but specialized for notes' single-agent dual-mode host/subscriber shape. sur/notes.hoon: new v1 core with request-id / action / command / response / response-update / response-body / response-update-body / incoming-request / requests / poke-status / action-error. state-11 adds requests=(map request-id incoming-request). lib/notes-json.hoon: enjs:v1 encoders (response, response-body, response-update, response-update-body, u-notebook-bare, action-error, poke-status); dejs:v1.action wrapper. tang-json is a placeholder emitting an empty array — typed errorType carries the actionable info. mar/notes/{action-1,command-1,response-1,response-update-1}.hoon: new versioned marks. Old %notes-action / %notes-command preserved. app/notes.hoon: - state-11 + state-10-to-11 migration arm - HTTP routes POST /notes/~/v1, GET /notes/~/v1/request/<uv> - finalize-request / finalize-pending / register-request helpers - give-http-response / http-error helpers - per-request behn timeout (20s) → %pending - cleanup-requests behn timer (5m) with channels' retention rules - se-core: rid / last-update / finalized door fields; se-poke-v1 wraps se-poke and emits the terminal response-update; se-update records the [time u-notebook] for that emission; se-finalize-with for typed early-finalize - no-core: no-action-v1 (watch + poke + behn-wait), no-agent-req-watch (transforms response-update → response, finalizes, leaves host watch), no-agent-req-poke (poke-ack handling) - watch paths: host-side /v1/notes/<flag>/request/<requester>/<uv> with src.bowl == requester assertion; subscriber-local /v1/request/<uv> with terminal-replay-on-subscribe tests: 4 new v1 tests + helpers (poke-a-v1, poke-c-v1, raw-noun introspection of cards via lark axes — ;; on cage was 90x slower). All 59 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches pokeAction (the underlying primitive used by pokeNotebook / pokeNoteAction / pokeFolderAction / pokeTopLevel — all ~40 call sites) from the Eyre channel PUT-then-watch-poke-ack flow to a synchronous HTTP POST against the new v1 API. Wrapper helpers stay the same. Behavior change: callers now resolve when the agent emits a typed terminal response (ok / no-change / pending) rather than when the channel's poke-ack arrives. %error bodies throw with errorType attached so future conflict-banner work can switch on the failure mode. Eyre-channel SSE remains for /v0/notes/.../stream subscriptions. - generateRequestId: crypto.getRandomValues → 96-bit base32 with dot separators every 5 chars from the right (canonical @UV form) - pendingPokes + POKE_ACK_TIMEOUT removed — HTTP holds the request open and returns the terminal body inline (or %pending on agent 20s timeout); no longer need to demux poke-acks out of the SSE stream Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hand-written from the agent's enjs/dejs encoders and the request / response wire types in sur/notes.hoon v1 core. Covers the POST and GET endpoints, the request envelope + response body union, all four action discriminator levels (a-notes → a-notebook → a-folder/a-note), the r-notes / u-notebook tree returned in %ok responses, and the action-error enum. Validates clean under @redocly/cli (2 warnings: missing license + localhost server — fine for internal use). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to openapi.yaml. Documents the three subscription channels
the browser uses over Eyre's SSE channel:
- /v0/notes/{host}/{name}/stream — per-notebook event stream
- /v0/inbox/stream — cross-cutting invite + notebook-list events
- /v1/request/{requestId} — per-request terminal response (only used
when the original POST returned %pending)
The Eyre channel transport (PUT-to-subscribe + EventSource over a
shared per-tab channel) is described in the top-level info because
AsyncAPI has no first-class model for it. Schemas mirror openapi.yaml
rather than $ref'ing across files so generators that don't follow
cross-file references still work.
Also gitignores the rendered preview HTML in docs/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle docs/openapi.yaml as JSON into desk/lib/notes-openapi.hoon and add a public GET route at /notes/openapi.json so an MCP proxy can fetch the spec without inventing an auth scheme. JSON because %mcp-proxy parses cached specs with de:json:html and rejects YAML. scripts/build-notes-openapi.sh regenerates the ++json arm from the YAML via js-yaml; mirrors the index-arm regen pattern in build-notes-ui.sh. Also adds docs/asyncapi.html — a small single-file viewer for the subscription spec (fetches asyncapi.yaml client-side). Verified end-to-end against tloncorp/mcp + a PR-resolved spec converter (tloncorp/mcp#3): notes upstream registered, hasCachedSpec=true, the generated notes_submitAction tool's inputSchema fully resolves the Action discriminator union, a tools/call with a 4-deep nested body creates a note end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cookie auth is fine for the user driving their own ship's notes UI, but a bot-ship running %mcp-proxy doesn't have a human to re-paste a 30-day-rotating cookie. State-12 adds an api-key=(unit @t) field minted on init (and after state-11 → state-12 migration), with a matching X-Api-Key header bypassing the v1 dispatch's src.bowl guard. - sur/notes.hoon: state-12 + new %regenerate-api-key / %clear-api-key a-notes variants - app/notes.hoon: - state-11-to-12 migration; init/load mint a key if api-key is ~ - +request-authorized: eyre-cookie OR matching X-Api-Key - +handle-v1-post gates on request-authorized then calls dispatch directly (bypasses the +poke %notes-action-1 src.bowl check, which would otherwise reject unauthenticated-eyre + valid-key) - +dispatch-v1-action extracted from +poke %notes-action-1 so both the poke path and the HTTP path share one implementation - invite/join helpers moved from +poke's |^ to the top level so dispatch can reach them - scry /x/v0/api-key for inspecting the current key (gated to src.bowl == our.bowl) - tests: 5 new — minted-on-init, regenerate-changes-value, clear-disables-bypass, x-api-key-bypasses-cookie, x-api-key-wrong-rejects. All 64 tests pass. Verified manually via curl: no auth → 401, wrong key → 401, right key without cookie → 200 with typed response body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the membership-wire pokes (handle-send-invite, join-remote,
leave-remote) used the legacy %notes-command mark with no request-id —
fire-and-forget cross-ship pokes. The sender's HTTP response resolved
on local poke-queue, not on the host's actual decision; an %accept-
invite that the host nacked left the FE thinking it worked.
This commit puts every cross-ship flow through the v1 request/response
machinery:
- send-v1-request: shared watch + poke-notes-command-1 + behn-wait
helper, keyed by flag in the wire so signs route through the standard
no-agent-req-watch / no-agent-req-poke handlers
- handle-send-invite-v1, join-remote-v1, leave-remote-v1,
handle-accept-invite-v1 replace the fire-and-forget versions
- dispatch-v1-action %join / %leave / %accept-invite / notebook
%invite no longer synchronously finalize — the response-update
closes the loop asynchronously
- +poke %notes-command-1 %notify-invite emits a %no-change
response-update so the sender's no-agent-req-watch finalizes
- no-agent-req-watch kicks off no-start-watch on the first successful
cross-ship response when net is %sub with init=|, so a freshly-
joined subscriber gets the broadcast stream after the join
- Watch handler relaxation: dropped ?> =(our.bowl ship.flag) on
/v1/notes/{flag}/request/{requester}/{uv}. The path's flag is the
notebook's identity, not who's hosting the response (the invitee
hosts response paths keyed by the inviter's flag). The
src.bowl == requester impersonation check stays.
Compat: dropped the legacy %notes-command poke arm and the four old
helpers (handle-send-invite, join-remote, leave-remote,
handle-accept-invite). External ships running pre-Phase-2 notes can
no longer reach this version. Acceptable per the consistency
directive.
Legacy %notes-action arm now synthesizes a rid from
`(mix eny.bowl rid-counter)` and routes through dispatch-v1-action.
rid-counter is a new state-13 field — tests don't advance bowl.eny
between events, so without the counter consecutive pokes would collide
on the per-request watch wire. State migration: state-12 → state-13
just adds rid-counter=0.
64 tests pass (1 newly-rewritten cross-ship membership-poke test
updated to use notes-command-1). 12 e2e tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… spec
Three issues raised in PR review:
P1 — GET /notes/~/v1/request/{requestId} was unauthenticated. The
request-id is not a capability: stored response bodies can carry
note updates and typed error details, and they're polleable by
anyone who knows (or guesses, or sniffs) the rid until cleanup runs.
+handle-v1-get-request now runs (request-authorized inbound-request)
before serving — same gate as POST. Tests:
- test-v1-get-request-requires-auth: unauthenticated GET → 401
- test-v1-get-request-honors-api-key: GET with matching key → 200
P2 — Failed remote joins left a ghost notebook placeholder. The
pre-v1 %notes-action poke-ack handler used to clean it up; the new
v1 path didn't. +no-cleanup-placeholder rolls back the books entry
(by setting gone=&, which no-abet honors) when net is %sub with
init=|. Wired into both nack paths:
- no-agent-req-poke poke-ack nack
- no-agent-req-watch %watch-ack nack and %fact %error
test-failed-join-cleans-up-placeholder exercises the poke-ack nack
path end-to-end via do-agent against the actual emitted poke wire.
P3 — Served OpenAPI spec was stale: described eyre-cookie-only auth,
missed the new %regenerate-api-key / %clear-api-key action variants.
docs/openapi.yaml updates:
- Top-level description: lists both auth schemes and their use cases
- Action discriminator: adds regenerate-api-key + clear-api-key arms
- securitySchemes: adds xApiKey alongside eyreCookie
- security: lists both (OpenAPI "OR" semantics)
scripts/build-notes-openapi.sh re-run so the embedded ++json arm
matches.
Also adds two small test helpers: +http-get-v1 mirrors http-post-v1
for GET inbound-requests; +http-status extracts a status code from
the response-header fact via !< on the small response-header vase
(the slow `;;(cage ...)` clam this still avoids).
67 tests pass (3 new); curl smoke against live ship confirms 401/401/
404 for no-auth / wrong-key / right-key-but-rid-evicted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the Eyre-channel poke surface with a request/response HTTP API at
/notes/~/v1. Every action — whether it lands locally or hops cross-ship — carries a client-generatedrequestId(@uv); the agent holds the POST open until the host's terminalresponse-updateis forwarded back, the host nacks, or a 20s timeout fires. Modeled on the venter-like pattern from tloncorp/tlon-apps#5334, specialized for notes' single-agent dual-mode host/subscriber shape.The bigger architectural shift: there's now one dispatch path through the agent (
dispatch-v1-action). Local actions, cross-ship actions, the FE, MCP clients, dojo pokes, and the legacy%notes-actionmark all funnel through it.What's in the branch (7 commits, oldest→newest)
b483497++ v1core insur/notes.hoon(action / command / response / response-update / response-body / response-update-body / incoming-request / poke-status / action-error).state-11addsrequests=(map request-id incoming-request). New marksmar/notes/{action-1,command-1,response-1,response-update-1}.hoon. Agent: HTTP POST + GET routes, per-request behn timeout, cleanup timer, full subscriber↔host lifecycle (no-action-v1 / se-poke-v1 / no-agent-req-watch / no-agent-req-poke). 4 new tests + 55 existing pass.a419c4bpokeActionswitched from Eyre-channel PUT-then-watch-poke-ack tofetch('POST /notes/~/v1'). All ~40 wrapper call sites (pokeNotebook,pokeNoteAction, etc.) unchanged.pendingPokesinfra removed — HTTP holds the request open and returns the typed terminal body inline.22c1acfdocs/openapi.yamlcovers the two endpoints, the discriminatedAction/ResponseBodyunions, the fullr-notes/u-notebooktree returned in%okresponses,ActionErrorenum, eyre-cookie security scheme. Validates clean under@redocly/cli.9e7201cdocs/asyncapi.yamldocuments the three SSE subscription channels (per-notebook stream, inbox events, per-request response stream) over Eyre's PUT-to-subscribe + EventSource transport. Plusdocs/asyncapi.html, a single-file viewer.8b9bf5eopenapi.jsonfrom the agent.desk/lib/notes-openapi.hoonembeds the spec;scripts/build-notes-openapi.shregenerates fromdocs/openapi.yamlviajs-yaml. Public route so MCP proxies can fetch the spec without inventing an auth scheme. JSON because %mcp's converter parses withde:json:htmland doesn't accept YAML.12d67a3state-12addsapi-key=(unit @t)minted on init / after migration. New a-notes variants%regenerate-api-key/%clear-api-key.+request-authorizedaccepts either eyre-validated cookie OR matchingX-Api-Keyheader.+handle-v1-postbypasses the+poke %notes-action-1arm'ssrc.bowlguard when api-key matches — calls+dispatch-v1-actiondirectly. Scry/x/v0/api-keyfor the local user to inspect/copy. 5 new tests.f2372cehandle-send-invite,join-remote,leave-remote) were fire-and-forget with the legacy%notes-commandmark. Now they all use+send-v1-request(watch + pokenotes-command-1+ behn-wait) and finalize asynchronously when the host's response-update arrives.+poke %notes-command-1%notify-inviteemits a%no-changeresponse-update.no-agent-req-watchkicks offno-start-watchon first successful cross-ship response so freshly-joined subscribers get the broadcast stream. Watch handler relaxed to allow invitee-hosted request paths keyed by inviter's flag. Legacy%notes-commandarm removed (pre-Phase-2 sub ships unsupported, per design). Legacy%notes-actionarm routes throughdispatch-v1-actionwith a(mix eny.bowl rid-counter)-synthesized rid;state-13addsrid-counterso test-agent (which doesn't advancebowl.eny) doesn't collide on the per-request watch wire.Test surface
mcp__sidwyn__run-testson/tests/app/notes)npm run test:e2e) — including the cross-ship invite / accept / leave / edit propagation specs/notes/openapi.json; calledsubmitActionwith a 4-deep discriminated body)Things deliberately out of scope
%conflicterror.se-update-note'sexpected-revisionmismatch still crashes the poke, surfacing as%error %unknownon the response-update path rather than%error %conflict. Single arm fix; left for later so the FE's conflict-banner refactor can land alongside it.%okpayloads for top-level actions.%create-notebook,%accept-invite, etc. finalize with%no-change. FE relies on the broadcast SSE stream for state sync.%notes-commandmeans another ship still running pre-Phase-2 notes can't poke this version — an explicit trade-off peroption Bin the design discussion.Related upstream
tloncorp/mcp#3 — patches
mcp-proxy.hoon's OpenAPI converter to actually resolve$refs into the generated MCP tool'sinputSchema. Required fornotes_submitActionto expose itsActiondiscriminator union to LLM clients. Without it the inputSchema is empty.🤖 Generated with Claude Code