desk: Bifrost half of the Fork Desk answer binding (ports, routes, per-call deciders) #40

Merged
heimdall merged 13 commits from heimdall/desk-bifrost-half into main 2026-11-11 01:02:37 +00:00
Member

Bifrost's half of the Fork Desk answer binding (follow-ups chat-s5852-5853, s5910-5911, s5939-5940, s5951-5951). Do not merge or deploy before Eureka's door cut exists. Huginn #31 is merged (664be6e).

What it is

/api/desk/{mind}/forks, /forks/{fork_id} and /answers behind one pipeline: bearer, audience bifrost-desk, scope, Active member (before the body is read), body shape, {mind} lookup, door call, outcome mapping. Auth and the door are ports (IDeskCredential, IDeskDoor). No production implementation of either is registered, and the routes are mapped only when the Desk config section exists; with it configured but no ports registered, startup fails loudly. Production is unchanged.

  • The decider list is per call (CultLib #78, 7d2cde04): DeskDoorContext(Mind, Authority, Subject, Deciders) travels with every answer, read and list; admin-set in configuration, 1..64 unique ids (CultLib #80), checked at startup and again before every door call; no HTTP field, header, query or body key can alter it (a body naming deciders is body_unknown_field).
  • Bifrost holds no fork state, computes no hash, stamps nothing. A stale_fork attaches the current card from ReadAsync; any door fault or out-of-contract outcome is 503 unavailable.
  • Vectors vendored at CultLib 89755b08 (sha256 3387debd…), pin-tested. Replayed through HTTP: 22 authCases, 57 mindRoutes, sameResponse pairs, results, scenarios (matrix_event steps skipped: no Matrix ingress).
  • No migration, no new package.

Verification

Desk filter 124/124. Full Bifrost.Web.Tests: 730 pass / 38 fail on the branch before round 2; the same 38 fail on origin/main 73c6f50 (21 BridgeCliTests where.exe, 8 ConceptTallyExport, 6 OidcIssuer, 2 ProviderAdvertisement, 1 ConceptVoteRateLimit; environmental under net8-on-net10 roll-forward). Branch-only failures: none.

Soul verdict (one pass, sonnet): Holds

About 70 hostile probes (auth order, forbidden keys including answerer, BOM/invalid UTF-8/%00/double encoding/5000-char Mind, query/header/cookie never reaching the door context, 503 Retry-After, 405s, CORS preflight): none breached. 12 of about 30 hand mutants survived; round 2 closed them with tests (wrong-Mind deciders, exact MaxBodyBytes boundary, retryAfterSeconds from config, case-sensitive audience, no-store, NotFound card read, shared socket, throwing port → 503) and Hands re-killed 20 hand mutations. The second Soul pass on the fixes was skipped by the quota rule.

Survivors and deviations

  • Survivor: body-read loop cap vs cap+1 (the shape judge enforces the cap too; the two cover each other).
  • Route shape is judged before the bearer: an unauthenticated /api/desk/x/answers/ gets 404 fork_unknown, not 401 (leaks route grammar only; contract says bearer first).
  • limit=007 and limit=1%32 are accepted.
  • AllowedOrigin (required), MaxBodyBytes, RetryAfterSeconds added to Desk config; empty Minds list fails startup; a list query without limit defaults to 50; an unparseable target is 404 fork_unknown with no auth check; repeated JSON keys, invalid UTF-8 and over-cap bodies are body_invalid.
  • Not reviewed second time: round 2 (commits a530aaa, f03027d, ef862f2).

Gaps for the wire (Huginn / Eureka / Libby)

Door framing and encoding (Bifrost has no MessagePack package); the read request must return the card with options_hash computed by Huginn; MindRefusal collapsed at the door to an unavailable-class answer; one socket per Mind, SO_PEERCRED check on Huginn's side; contract prose says 200 for stored Refused while vectors give 4xx (CultLib #77 may have fixed it); no rate_limited code, so no rate limit; the bearer, audience and scopes are Bifrost PR #37's cut (blocked).

🤖 Generated with Claude Code

Bifrost's half of the Fork Desk answer binding (follow-ups chat-s5852-5853, s5910-5911, s5939-5940, s5951-5951). **Do not merge or deploy before Eureka's door cut exists.** Huginn #31 is merged (664be6e). ## What it is `/api/desk/{mind}/forks`, `/forks/{fork_id}` and `/answers` behind one pipeline: bearer, audience `bifrost-desk`, scope, Active member (before the body is read), body shape, `{mind}` lookup, door call, outcome mapping. Auth and the door are ports (`IDeskCredential`, `IDeskDoor`). **No production implementation of either is registered, and the routes are mapped only when the `Desk` config section exists**; with it configured but no ports registered, startup fails loudly. Production is unchanged. - The decider list is **per call** (CultLib #78, 7d2cde04): `DeskDoorContext(Mind, Authority, Subject, Deciders)` travels with every answer, read and list; admin-set in configuration, 1..64 unique ids (CultLib #80), checked at startup and again before every door call; no HTTP field, header, query or body key can alter it (a body naming `deciders` is `body_unknown_field`). - Bifrost holds no fork state, computes no hash, stamps nothing. A `stale_fork` attaches the current card from `ReadAsync`; any door fault or out-of-contract outcome is 503 `unavailable`. - Vectors vendored at CultLib 89755b08 (sha256 3387debd…), pin-tested. Replayed through HTTP: 22 authCases, 57 mindRoutes, sameResponse pairs, results, scenarios (matrix_event steps skipped: no Matrix ingress). - No migration, no new package. ## Verification Desk filter 124/124. Full `Bifrost.Web.Tests`: 730 pass / 38 fail on the branch before round 2; **the same 38 fail on origin/main 73c6f50** (21 BridgeCliTests `where.exe`, 8 ConceptTallyExport, 6 OidcIssuer, 2 ProviderAdvertisement, 1 ConceptVoteRateLimit; environmental under net8-on-net10 roll-forward). Branch-only failures: none. ## Soul verdict (one pass, sonnet): Holds About 70 hostile probes (auth order, forbidden keys including `answerer`, BOM/invalid UTF-8/`%00`/double encoding/5000-char Mind, query/header/cookie never reaching the door context, 503 Retry-After, 405s, CORS preflight): none breached. 12 of about 30 hand mutants survived; round 2 closed them with tests (wrong-Mind deciders, exact MaxBodyBytes boundary, retryAfterSeconds from config, case-sensitive audience, no-store, NotFound card read, shared socket, throwing port → 503) and Hands re-killed 20 hand mutations. The second Soul pass on the fixes was skipped by the quota rule. ## Survivors and deviations - Survivor: body-read loop cap vs cap+1 (the shape judge enforces the cap too; the two cover each other). - Route shape is judged before the bearer: an unauthenticated `/api/desk/x/answers/` gets 404 `fork_unknown`, not 401 (leaks route grammar only; contract says bearer first). - `limit=007` and `limit=1%32` are accepted. - `AllowedOrigin` (required), `MaxBodyBytes`, `RetryAfterSeconds` added to Desk config; empty Minds list fails startup; a list query without `limit` defaults to 50; an unparseable target is 404 `fork_unknown` with no auth check; repeated JSON keys, invalid UTF-8 and over-cap bodies are `body_invalid`. - Not reviewed second time: round 2 (commits a530aaa, f03027d, ef862f2). ## Gaps for the wire (Huginn / Eureka / Libby) Door framing and encoding (Bifrost has no MessagePack package); the read request must return the card with `options_hash` computed by Huginn; `MindRefusal` collapsed at the door to an unavailable-class answer; one socket per Mind, SO_PEERCRED check on Huginn's side; contract prose says 200 for stored Refused while vectors give 4xx (CultLib #77 may have fixed it); no `rate_limited` code, so no rate limit; the bearer, audience and scopes are Bifrost PR #37's cut (blocked). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Records for the CultLib fork-card contract (answer_request, refusal,
unavailable, card, desk_answer, answer_result), DeskBodyShape (cap, forbidden
keys at any depth, closed keys, types and formats, UTF-8 byte note limit) and
fork-card-vectors.json vendored from CultLib 6231ecad with a sha256 pin. Tests
replay the vector arrays row by row. Nothing is mapped yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
DeskOptions binds Minds as an ordinal array with sockets and per-Mind
deciders, plus Operator/Stewards attribution. StartupConfigurationValidator
refuses blank or repeated ids, relative sockets, non-lowercase-D account ids
and an Operator listed as steward. DeskMindRoutes.Resolve percent-decodes the
raw segment once (strict UTF-8) and compares ordinally; tests replay every
mindRoutes vector segment. No route is mapped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hand mutation showed strict UTF-8, non-ASCII refusal and bad-escape refusal
were unobserved; a configured id that spells the failure now exposes each.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /api/desk/{mind}/answers behind one pipeline: raw target, bearer, audience,
scope, Active membership, body shape, {mind} lookup, door. Mapped only when the
Desk section exists; a configured desk without its credential and door ports
stops startup. Stale_fork answers carry the current card read through the door.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET /api/desk/{mind}/forks and /forks/{fork_id}; the query is judged as
request_invalid before the Mind lookup. Every door call (answer, read, list)
carries the configured deciders; the vectors' 22 auth cases and 57 route cases
now replay for all three routes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Existing rows are byte-identical; deciders_at_the_cap_of_64 is a new valid card row.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The door context carries mind, authority, subject and deciders (fork-card.md M5).
Deciders are 1 to 64 unique ids per Mind, refused at startup and again before any
door call. A throwing credential port or account lookup answers 503 unavailable.
Startup refuses two Minds sharing one socket. Behavioural tests for the per-Mind
list, the body cap to the byte, Retry-After, audience case, no-store, cancellation
and the stale-fork card read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!40
No description provided.