8.7 KiB
Host JSON API
handle(&Api, Caller, Request<Bytes>) answers requests without a transport.
The wasi:http hook supplies the caller from the process table.
It must not accept caller identity from headers, environment variables, or JSON.
Reads are open to every caller.
Credential JSON routes return 200 on success.
Credential errors use {"error":"kind","message":"kind"} with static, secret-free text.
Missing routes return 404. Invalid parameters return 400. Authorization failures return 403.
Conflicts return 409. The filing budget returns 429.
World web mount
The world runs web.wasm on port 8080 and serves fresh files from /srv/www.
It forwards /api/PATH to http://host.sausage/PATH through WASI HTTP, including streaming bodies.
The host opens no separate network port.
Browser ceremonies derive the passkey RP ID and origin from the request Origin header.
The host retains separate pending ceremony state per origin against the same credential registry.
After the first registration, every decision and further registration requires a passkey assertion.
The host serves no HTML or UI assets. The world web UI owns credential views in /srv/www.
The Passkeys view lists labels and registers passkeys. The bootstrap notice opens that view.
The approval view at /#approve/ID reads stored summaries and details through JSON routes.
The secret-entry view at /#entry/ITEM/FIELD encrypts the secret before submission.
The host constructs authorization bindings from stored requests and verifies every proof.
Public JSON routes
GET /whoami: the transport's caller.GET /cred/items: public item snapshots.GET /cred/items/{id}: one public snapshot.GET /cred/requests?state=pending: requests with diffs and summaries. Omitstateto return every request.GET /cred/requests/{id}: one request with its diff and summary.POST /cred/requests:{"kind":"grant","key":"stable-key","request":{...}}. The request containsitemsand optionalduration_secs,max_uses, andvalidator. KindeditacceptsOperationRequestwithitem,base_version, andchange. The response is{"request":"id"}. Filing does not approve anything.GET /cred/grants: the grant ledger.
Explicit request keys cannot begin with inject: or ssh:.
Those prefixes belong to automatic requests.
Explicit requests, automatic SSH requests, and new ceremonies share a per-process budget.
The budget permits 32 attempts per 60-second window.
Reads and granted SSH operations do not spend that budget.
Harness routes in the world
The WASI transport dispatches buffered and streaming responses through one route table.
Harness routes use http://host.sausage/harness/ and the console's input channel.
The browser reaches these routes under /api/harness/.
The V8 harness answers queries and invokes actions without a second storage reader.
GET /harness/conversations:{conversations:[...]}with records, titles, agents, starters, concluded state, and running state. Records retain their owner and parent references.GET /harness/conversations/{id}/entries?after=0:{entries:[...]}in ascending entry order.GET /harness/conversations/{id}/stream?after=0: newline-delimited entry records, then new entries as commits arrive.POST /harness/conversations/{id}/submit:{content:"text",requestId:"stable-id"}. The response is 202 with{submissionId:number}. Duplicate request IDs use pi-durable's existing idempotent admission.POST /harness/conversations/{id}/stop: abort the conversation's active turns, including their owned work. The response is{ok:true}. Queued inputs remain.GET /harness/events?after=0: newline-delimited T43 event records in ascending event order.POST /harness/extensions/{name}/actions/{action}:{args:{...},conversationId?:number}. Only names in the extension'suserActionsexport are callable. The response is{result:...}. Disconnect aborts the action's signal.GET /harness/models:{models:[...]}with public catalog fields, without provider authentication data.GET /harness/families:{families:{...}}from the world's model definitions.GET /harness/extensions: the loaded extension status, also visible through/proc/extensions.GET /harness/credential-requests:{requests:[...]}with pending conversation requests andapprovalUrl: "/#approve/ID"links. The web UI opens its internal approval view.
The after cursor is an exclusive entry ID. Streams replay retained records before live records.
Commit observation starts before replay, so a concurrent commit cannot create a gap.
Clients resume from the last record ID after a disconnect.
Disconnect closes the observation. Harness shutdown closes all streams.
Responses use JSON, except streams use application/x-ndjson.
Invalid inputs return 400. Missing routes, conversations, and actions return 404.
An unavailable harness returns 503. Action failures return 500 with the action's error message.
Decisions
POST /cred/requests/{id}/challenge:{"decision":"approve","overrides":{...}}. Decisions areapproveordeny. Overrides use T27's typedOverridesstructure. Unknown fields fail. A denial accepts only default overrides. With no passkey, the response is{"unlocked":true}. Otherwise, the response is{"id":"challenge id","options":{...}}.POST /cred/requests/{id}/decide:{"challenge_id":"id","assertion":{...}}. The host reads the decision and overrides from the verified binding. An optional explicit decision must match that binding. The library checks the request ID, decision, overrides, and stored payload digest inside its transaction. Without passkeys, this route accepts{"decision":"approve","overrides":{...}}instead. The library checks itsUnlockedproof inside the transaction.POST /cred/grants/{id}/revoke:{"key":"stable-key"}. This route files a revocation request and returns{"request":"id"}. Use the normal challenge and decision routes to approve it.
Successful mutations return {"ok":true}.
A successful assertion produces a non-cloneable Verified proof.
A replay or a mismatched binding cannot authorize another action.
Passkeys
GET /cred/passkeys: public credential IDs and labels, not serialized passkeys.POST /cred/passkeys/register/challenge:{"label":"new label"}. The returned assertion challenge binds the exact label.POST /cred/passkeys/register/start:{"label":"new label","gate":{"id":"challenge id","response":{...}}}. The first registration permits an absent gate, as D53 requires. Every later registration needs the existing passkey's enrollment assertion.POST /cred/passkeys/register/finish:{"challenge_id":"id","response":{...}}.
WebAuthn options and responses use the library's browser JSON formats. The host derives the RP configuration from each browser origin and retains pending ceremony state. Restart discards pending ceremonies but retains registered passkeys.
Secret entry
GET /cred/entry/key:{"public_key":[...]}in SEC1 format.POST /cred/items/{id}/fields/{field}/challenge:{"ciphertext_sha256":"lowercase hex"}. The host loads the field metadata and constructs the binding itself.POST /cred/items/{id}/fields/{field}:{"challenge_id":"id","assertion":{...},"envelope":{...}}.
The field parameter is its stable ID.
Secret entry requires an assertion. Register a passkey first.
The host compares the item, field, and envelope hash before it calls the encrypted field API.
That API checks the typed proof and metadata before it decrypts inside its transaction.
Public fields cannot use secret entry.
Non-UTF-8 plaintext and invalid envelopes fail without an echo.
See cred/entry.rs for the ECDH, HKDF, AES-GCM, and hash formats.
SSH
POST /ssh/identity:{"host":"example.com"}. The response is{"item":"id","public_key":"base64 SSH public key blob","user":null}. A publicuserfield supplies the optional default user.POST /ssh/sign:{"host":"example.com","item":"id","payload":"base64"}. The response is{"signature":"base64 SSH wire signature"}.
SSH operations match SSH rules and enforce free use or a caller grant.
Both operations evaluate item and grant validators through QuickJS, as D58 requires.
Identity discovery checks access without spending a signing use.
Signing reserves one use. Preparation commits its pending audit row in the same transaction.
Successful operations complete that row. Signing records only the payload hash, never the payload.
An ungranted caller receives 403 with error, message, item, and request.
The host files the request before it responds.
Two matching items produce 409. The signing item must match the selected host rule.
No SSH response returns a private key or decrypted field value.