Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): consult anonymous-deny gate before /security's 503 - #7958
Conversation
handleSecurityRequest resolved the security service and returned 503 "Security service not available" for an empty/non-duck-typing slot BEFORE reaching the !ec || shouldDenyAnonymous(...) gate ~20 lines below, so an unauthenticated caller to /api/v1/security/suggested-bindings got a capability disclosure (503) instead of the admin-surface refusal (401 UNAUTHENTICATED) this handler's own comment calls unconditional (#2567, #3963). Straight hoist, mirroring the #7653/#7910 fix on domains/ai.ts: the gate now runs first and decides once. The !ec arm is unchanged (documented #4127 batch 3 as behaviour-preserving) so this changes WHEN the decision is made, not WHAT it decides. The 503 answer stays unchanged for an authenticated caller against an empty/stubbed slot, and a serveable slot still works authenticated and still denies anonymous. No route-level auth: false opt-out exists on this domain, so there is a single consult site. Adds packages/runtime/src/domains/security-anonymous-deny-ordering.test.ts (10 cases, mirroring ai-anonymous-deny-ordering.test.ts's group shape) and a patch changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
hotlong
commented
Aug 12, 2026
PM review — The docs-drift advisory is a false positive — and it corroborates the fixChecked with a control (a zero-hit grep proves nothing unless the scanner is known to work):
Better than neutral, though: One fidelity note on the test double, for whoever copies it nextThe header's claim — that
Verified harmless today: The latent edge is only this: if that 503's wording ever changes to something the heuristic matches, Group B keeps passing while production ships Everything the dispatch asked for is presentHoist with all Holding out of the merge queue until the queued checks finish. Generated by Claude Code |
hotlong
commented
Aug 12, 2026
CI red — 139 files / 2137 tests otherwise green. This is a good failure. A pre-existing test pinned the exact behaviour this PR intentionally changes, and it caught it — which is the ratchet doing its job, not a defect in the fix. It was missed locally because the run was scoped to On the repair, since the tempting fixes are both wrong. Read the test's name: its purpose is to prove the 503 originates inside the handler — it is not a statement about anonymous ordering, and it only ever reached that 503 anonymously as an artefact of the harness. The test immediately below it already covers anonymous denial against a wired service. So: ⛔ not deleted, and ⛔ not re-pointed at 401 — that would collapse it into a duplicate of its neighbour and leave nothing in the suite proving the in-handler 503 path survives, which is precisely the negative control this change needs. Instead it adopts a pattern already present and documented in the same file: Ledger constraint restated for the patch round: Generated by Claude Code |
CI caught this correctly: domain-handler-registry.test.ts's "/security responds 503 when no security service is wired (legacy in-handler semantics)" reached its 503 via dispatch(), which re-resolves identity from the mock kernel and answers anonymously — so after #7911's hoist the anonymous-deny gate now intercepts it first and returns 401. The test's NAME says its job is to prove the 503 comes from INSIDE the handler, not to assert anonymous-vs-authenticated ordering (the next test down already covers anonymous denial with a wired service). Flipping the assertion to 401 would have destroyed that purpose and left nothing pinning the in-handler 503 path #7911's report calls out as the required negative control. Repaired using the pattern already established in this file at :188-190 for /notifications: call the public handleSecurity() delegate directly with a seeded AUTHENTICATED executionContext, bypassing dispatch()'s identity re-resolution, so the test proves what its (renamed) name claims — no service wired => 503 from inside the handler, once the gate has been cleared — without also asserting the ordering #7911 just fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7911
The defect
handleSecurityRequest(packages/runtime/src/domains/security.ts) resolved thesecurityservice and returned 503 "Security service not available" for an empty or non-duck-typing slot — before it reached the!ec || shouldDenyAnonymous(...)gate ~20 lines below. So on a deployment where thesecurityslot is empty or stubbed, an unauthenticated caller to/api/v1/security/suggested-bindingsgot a 503 capability disclosure instead of the 401 UNAUTHENTICATED refusal this handler's own comment calls unconditional (#2567, #3963)./securitystands on the same anonymous-deny floor as/data,/meta,/actionsand/automation(ADR-0056 D2 → #3963). This was the last of the six dispatcher domains still ordered the wrong way — the sibling inversion on/ai/**was #7653, fixed in PR #7910.The fix
Straight hoist, mirroring #7910's shape on
domains/ai.ts: move the!ec || shouldDenyAnonymous(...)block above theresolveServiceprobe. The!ecarm is documented (#4127 batch 3) as behaviour-preserving, so the hoist changes when the decision is made, not what it decides. No route-levelauth: falseopt-out exists on this domain, so there is a single consult site — simpler than/ai. All pre-existing explanatory comments are preserved, re-sited with the code they now sit next to.Pins
securityslot → 401, asserting botherror.code === 'UNAUTHENTICATED'and the HTTP status (ADR-0112).Tests
New
packages/runtime/src/domains/security-anonymous-deny-ordering.test.ts(10 cases), mirroringai-anonymous-deny-ordering.test.ts's group shape:Reverse verification: reverted the hoist (
git checkout origin/main -- packages/runtime/src/domains/security.ts), re-ran the new test file — the 4 Group A (401) cases failed withexpected 503 to be 401(right reason, not a compile error), Groups B/C stayed green. Restored the fix and re-ran green.check:type-check-debtratchet: re-measured@objectstack/runtime's TEST_DEBT the same way the gate does (siblingtsconfig.debt-remeasure.jsonlifting the test exclusion,tsc --noEmit) — 227 errors, unchanged from the recorded ledger value; the new test file adds zero.Scope
Does not touch
packages/rest/src/rest-server.ts(registerSecurityEndpointsis in flight on #7678, a different defect).Generated by Claude Code