Skip to content

test(dogfood): the anonymous door sweep — ADR-0138 criterion 2 as an in-repo conformance test - #23137

Draft
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-22146-anonymous-sweep-test
Draft

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-22146-anonymous-sweep-test

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22146

Clause-②: no (a test; no published accept set or public surface changes)

Summary

The test half of ruling 6134095444: ADR-0138 acceptance criterion 2 is evidenced by a test in the tree, not by a one-off sweep. This PR adds one dogfood suite and its classification table, nothing else:

  • packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts
  • packages/qa/dogfood/test/anonymous-door-sweep.classification.ts

No production code changes, and nothing under docs/adr/**. The criterion-2 wording and criterion 4's two back-pointers are the next PR of the ruling, after this test is green on main.

The suite is red on this head. That is the result, not a defect in the test. The everything-else class does not answer 401 throughout: one surface family outside D2's list answers an anonymous caller without the 401 floor (class-level result below). As the dispatch orders, the assertion stays as D2 states it. It is not weakened and not marked expected-to-fail, and the PM decides the next step.

What the suite does

  1. Boot. It boots the showcase the way a deployment serves it: bootShowcase, plus two extras. MetadataPlugin runs over a build-shaped artifact, because createStandaloneStack composes it and declared endpoints only route through it. MCPServerPlugin is added because serve mounts MCP by default.

  2. Population. It reads the registered route table off the running server, from three sources:

    • the port, getMountedRoutes();
    • the framework's own table, getRawApp().routes, which also holds the raw-app mounts;
    • the declared ADR-0121 endpoints the metadata service matches.

    Nothing in the population is listed by hand. The derivation is guarded by a floor per source, by asserting the port table is a subset of the framework table, by one anchor per source, and by checking that every declared endpoint resolves through matchEndpoint to itself.

  3. One unauthenticated request per route. Middleware is the exception, because it serves no request of its own.

  4. A closed table. Each answer is judged by the route's class: D2's six door classes, the surfaces outside the guest model by construction, middleware, and nothing else.

    • A route the table does not claim is in the everything-else class. It must answer 401 with an error envelope and nothing served, so a new anonymous door nobody classified turns the suite red.
    • A classification row that claims no route on the boot is red as stale.
  5. Every class on a real subject, admitted and refused. A sweep whose probes name nothing can only observe the refusal half of a door. The subjects are the showcase's own or are created in-test while signed in: the public form, the shareable brief, the public book, an uploaded file. The showcase has no subject for three of the legs, so they are declared inline beside it:

    • an anonymous object_operation endpoint and an anonymous flow endpoint, over a flow with no data step;
    • an org-audience book;
    • two docs.

    The showcase's own two declared endpoints are authRequired: true and of the same two types, so they serve as the negative controls of classes 4 and 5.

The assertions are class-level. A failure names the route pattern and the status class it answered, never a body.

Classification, for the ADR amendment to name

Door classes (D2):

class routes
1 · the public form doors resolve and submit
2 · share links the two token-addressed reads
3 · the public book and doc reads no route of its own: it rides the shared /meta read routes, which the sweep holds to 401 for every other subject
4 · declared authRequired: false endpoints, object_operation the declared endpoints
5 · declared authRequired: false endpoints, flow the declared endpoints
6 · the acl: 'public_read' file download both download routes

Outside the guest model by construction. Each row carries its reason in the table.

  • D2's own list:
    • the control-plane allowlist, by its own predicate isAuthGateAllowlisted, never a copy of its paths;
    • the ADR-0041 signed inbound hook, asserted 401 on a missing signature, as D2 states;
    • MCP, asserted 401, as D2 states.
  • The surfaces the ruling ordered classified explicitly:
    • the approvals plugin's token-addressed action pages (the ADR-0043 action token);
    • the OAuth and OpenID well-known documents;
    • RuntimeConfigPlugin's bootstrap reads;
    • the MCP skill document read;
    • the dev-only metadata HMR stream, which this boot mounts because bootStack runs the development posture.
  • Two more that the population held and the round-5 list did not name:
    • the discovery documents, at the two spellings isAuthGateAllowlisted does not name literally (the REST root and the dispatcher's well-known document);
    • the local storage adapter's signed byte URLs, where the signed, expiring token an authorized upload or download mints is the credential.

Middleware: the server-wide * registrations.

Class-level result on this head

class matches D2
1 · public form doors yes
2 · share links yes
3 · public book and doc reads yes
4 · authRequired: false object_operation yes (D3's empty state: the guest holds no grant, so it is denied)
5 · authRequired: false flow yes (admitted at the door as the guest; the flow's output only)
6 · acl: 'public_read' file download yes
everything else no: one family

The family, at function level only: the settings service's REST family (registerSettingsRoutes in @objectstack/service-settings, mounted by SettingsServicePlugin). It opens no handler with an anonymous floor, so it answers an anonymous caller from its own authorization logic rather than with 401. On this boot it served no settings value. The status class is still not the one D2 states. The finding is in the report on the card, for the seat to file.

Proof the suite can fail

Two ablations, each through scripts/ablation-replace.mjs (anchor hit, then restore proven by blob hash against HEAD and an empty git diff HEAD):

  • A, the closed table. One outside-family row (the runtime-config bootstrap reads) was deleted from the classification.
    • Mutated: the everything-else case named 4 → 6 routes, and every other case was unchanged.
    • Restored: blob eb86b66c06 == HEAD.
  • B, a product floor, through dist/. One dispatcher domain's anonymous floor (shouldDenyAnonymous in the /i18n domain of @objectstack/runtime) was bypassed in source, and runtime was rebuilt.
    • Mutated: ablation-dist-preflight found the marker in 2 built files, and the everything-else case named 4 → 7 routes.
    • Restored: blob d111be27d4 == HEAD, runtime rebuilt, the marker absent from all 6 built files, the tree clean, and the everything-else case back to the same 4.

Both moved in the predicted direction: more diagnostics, not a different failure.

Tests

Run on head bf3ba9957, which merges origin/main dfb3ba2b2:

  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/anonymous-door-sweep.dogfood.test.ts test/authz-conformance.test.ts: 74 passed, 1 failed. The failing case is the everything-else one, on the settings family. authz-conformance is green; it scans every *.dogfood.test.ts for row attribution, and the new file claims no row.
  • pnpm --filter @objectstack/dogfood typecheck (tsc --noEmit): exit 0. tsc --listFilesOnly lists both new files in the program.
  • eslint, narrowed to the 2 new files with --no-inline-config --format json: 0 errors, 0 warnings over 2 files. The narrowing is safe because eslint.config.mjs enables no type-aware linting (no parserOptions.project or projectService in the printed config), so no untouched file's result can change.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 53 commands from the merge base. All 53 were run on this head and every one exits 0. The --ran reconcile prints 53 derived famil(ies) accounted for — 53 run, 0 NOT-MEASURED (a DERIVED zero — all 53 recorded an exit code and none of them is 3). Two of them first exited 3 (PREREQUISITE NOT MET): one needed a pinned commit the shallow clone lacked, and one needed 8 packages that had no dist/. Each was re-run once its prerequisite was met, and both exit 0.

Acceptance notes

  • Where it runs. The suite lands in the isolated dogfood project by its file name; packages/qa/dogfood/vitest.config.ts is untouched, and PR test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) #22962 edits it. It runs with the dogfood battery, which is part of main's full battery. PR and queue runs carry the fast set, so this draft's own CI does not run it. Landing it while the settings family is open makes the dogfood battery red on main.
  • Two codes for one refusal. The everything-else 401s carry two codes: UNAUTHENTICATED, and AUTH_REQUIRED on the storage family's refusals. D2's text names UNAUTHENTICATED, while criterion 2 judges the status class and what is served. The suite asserts the status and the error envelope and does not pin the code. Noted for the ADR wording PR.
  • What a failure prints. A failure message names the route pattern and the status class. On main that text lands in CI logs. It never carries a body.
  • No changeset. The dogfood package is private, so this PR carries skip-changeset.

Parked in draft (written by the seat, domain:spec seat 2)

This PR stays draft until the settings family answers anonymous callers with 401. The dogfood battery is not in PR or queue CI, so this red shows in no check run here, and landing first would turn main's dogfood battery red.


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1ca7a805ca810bb9eb1c1f6910401f8c582129db → packageMentionDocs.

…unt family, with route detail behind a local-only switch

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants