Skip to content

refactor(rest): move registerSecurityExplainEndpoints and registerSecurityEndpoints into security-routes.ts (#23050 slice 6) - #23729

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23050-s6-security-endpoints
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23050-s6-security-endpoints

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23050
Clause-②: no

Slice 6 of the rest-server.ts split, by slice 1's pattern (landed note 6136102437, steps 1–6), slice 2's getter refinement (6189011700) and slices 3–5's shape (PR #23670, PR #23705's two registrars in one module, PR #23719): the bodies of RestServer#registerSecurityExplainEndpoints (GET / POST {basePath}/security/explain, GET {basePath}/security/my-delegable-scope) and RestServer#registerSecurityEndpoints (GET {basePath}/security/suggested-bindings and its /:id/confirm and /:id/dismiss) move, unedited, to ONE new module, packages/rest/src/security-routes.ts. It is move-only: routes, the ordered registrations, every handler, the published dist/*.d.ts and both bundles' export lists are byte-identical, and every reader whose reading included a moved line now reads it where it is, with its sum unchanged.

rest-server.ts goes from 11,042 to 10,636 lines (base a515abdd8c). The card stays open for the later slices.

The pattern, applied

Members the moved bodies read, from an AST walk of each method (TypeScript compiler API, base a515abdd8c): registerSecurityExplainEndpoints (JSDoc :10282, method :10308-:10601, body :10309-:10600) and registerSecurityEndpoints (JSDoc :10662, method :10694-:10838, body :10695-:10837).

kind member reads (explain / suggested-binding) class declaration
field routeManager 3 / 3, the register calls :2010, private
field kernelManager 2 / 0, inside the explain family's resolveService :2019, private, not readonly
field securityServiceProvider 2 / 2, inside each family's resolveService :2089, private, not readonly
method resolveRequestEnvironmentId 1 / 0 :2306, private
method resolveExecCtx 2 / 3 :2856, private
method enforceAuth 2 / 3 :2456, private

Neither body has a bare this, an element access on this, or a this-rebinding function. Their module-level reads:

  • Imports still used elsewhere in rest-server.ts (AST count outside both methods): logError (12), sendError as sendEnvelopeError (4) and refuseRepeatedQueryParams (15). They stay there; the module imports the same names from the same modules. Its sendError import carries a comment pointing at the dialect note on rest-server.ts's import, which the explain family's emitter docblock names.
  • Imports ONLY these bodies read (zero uses outside the two methods): ErrorCode (type, @objectstack/spec/api, with its [#8073] comment), standardErrorCodeForHttpStatus (@objectstack/spec/api, [#11683]), classifiedRefusalAnswer, nestedRefusalEnvelope, sandboxFaultAnswer (./error-response.js), and isAudienceBindingSuggestionStatus with unknownAudienceBindingSuggestionStatusMessage (@objectstack/core, [#7678]). They moved with the bodies, as slices 3 and 5 moved theirs, and each left a pointer comment. Leaving them would fail noUnusedLocals. The [#8073] and [#7678] comments moved verbatim. The [#11683] comment pointed at registerSharingEndpoints as its use, so in the module it names the explain family's classified-refusal arm, the use it has there.
  • Nothing declared in rest-server.ts is read, so no export was added anywhere. The explain body's dynamic import('@objectstack/spec/security') moved inside it.

Host shape (slice 1's shape B, twice, as slice 4): one host interface per registrar, SecurityExplainRouteHost and SecurityRouteHost. Each lists exactly the members its body reads, typed RestServer['m']. The class keeps both private methods with their names, docblocks and positions. Each body is now a host literal plus .call(host, basePath). Methods go as late-bound closures, (...a) => this.m(...a). The call sites in registerRoutes (:4812-:4813 at base) are unchanged.

Fields, checked one by one:

field read when reassigned after registration? handed over as
routeManager registration only (six register calls, top-level statements of the two bodies) its one swap (:5719 / :5723) wraps registerMetadataEndpointsInner, not these registrars value
kernelManager request time (the explain family's resolveService) not by the class (constructor only), but not readonly getter
securityServiceProvider request time (each family's resolveService) not by the class (constructor only), but not readonly getter, in both host literals

The getters are measured, not argued. A one-off vitest file (copied in, run, deleted) boots a source RestServer, drives the door, assigns the field on the instance AFTER registerRoutes(), and drives it again:

  • this head (96504dab66): securityServiceProvider on the explain family (GET /api/v1/security/my-delegable-scope) reads 501 NOT_IMPLEMENTED, then 200 {"businessUnits":["bu_1"],"positions":[]}. On the suggested-binding family (GET /api/v1/security/suggested-bindings) it reads 501 NOT_IMPLEMENTED, then 200 {"data":[{"id":"sug_1"}]}. kernelManager on the scoped explain door (environmentId: env_1) reads 501, then 200 {"env":"env_1"}. Tests 3 passed (3).
  • by-value ablations through scripts/ablation-replace.mjs, each getter in turn replaced by X: this.X, with a two-line anchor that names its host literal. Each landed: anchor 1 → 0, replacement 0 → 1, blob 4867a14302 → 5c7ebad6df / 48f2660f00 / f3c9b53365. In each run the ablated door reads 501 then 501 (frozen at registration) while the other two still read 200 (Tests 1 failed | 2 passed (3), each run). Restored by the tool and a trap: blob 4867a14302 equals HEAD, git diff HEAD empty, the one-off file deleted. The path is source (relative import), so no dist leg applies.

Mechanism hypotheses from the dispatch, measured:

  • (a) Confirmed. The explain family resolves the security service at request time through its resolveService. That reads TWO fields: the environment kernel's security service via kernelManager first, then securityServiceProvider as the fallback. Naming a principal other than the caller is the service's question (explainAccessForCaller); the door hands it the parsed request and the caller's context and reads no further provider field. Both fields go as getters. The suggested-binding family reads securityServiceProvider only, also a getter, measured separately.
  • (b) Confirmed. registerSecurityEndpoints builds its paths from const dataPath = basePath;, so the suggested-binding doors answer at {basePath}/security/suggested-bindings, not under /data (slice 4's share family appended crud.dataPrefix; this one does not). Every path expression moved unedited, and the route dumps below are identical.
  • (c) Confirmed. The bodies, and the [#7678] import comment that moved with them, carry 10 ADR-NNNN Dk citations: ADR-0090 D5 ×2, D6 ×2 and D12 ×2, ADR-0105 D8 ×2, and ADR-0112 D5 ×2. They left the anchored rest-server.ts, so check-adr-anchors read 1761 decision-letter citations at base and 1751 at the move-only commit d6fc91a97d, green both times. That is the silent narrowing the card forbids. A new anchor (below) restores 1761 at head.

The registrar list

packages/rest/src/rest-server-registrar-files.ts gains security-routes.ts with a [#23050 slice 6] comment (length 6 → 7). Every reader that pins its length re-reads its number, and every sum is unchanged:

reader sum over the list change
rest-server-query-number-census.test.ts 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 length 6 → 7
rest-server-canonical-query-ast.test.ts 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 query: slots floor security-routes.ts: 0, length 6 → 7
meta-draft-read-door-census.test.ts 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 the two bodies read no draft; length 6 → 7
rest-server-docblock-position.test.ts floors 4 + 3 + 3 + 8 + 3 + 8 = 29 floor security-routes.ts: 8: header, two host interfaces, two functions, and the three blocks that moved with the bodies (the explain family's respondError and delegableHandler blocks, the suggested-binding family's respondError block). rest-server.ts 167 → 164 blocks, floor 100; length 6 → 7
execctx-consumer-census.test.ts 62 sites = 27 + 9 + 5 + 7 + 8 + 1 + 5 mentions 101 → 109 (the seam twice: two forwarders ×2, two interface members ×2); forwarders 8 = (7 − 1) + EXTRA_MOVED_REGISTRARS 2, the map gaining security-routes.ts: 1 beside sharing-routes.ts: 1, as slice 4 counted two registrars in one module
ui-view-route-identity.measurement.test.ts / ui-view-route-tenancy.measurement.test.ts this.enforceAuth( 55, this.resolveExecCtx( 70 over the list (53 / 68 before, +1 each per new forwarder); ownership guard 1 length 6 → 7
dogfood authz-conformance.test.ts (realtime tripwires) 0 hits over the seven files length 6 → 7
dogfood authz-probe-blind-spot.census.ts, rest-server.ts row 70 / 19 / 51 unchanged; this.routeManager.register( 63 = 27 + 9 + 5 + 7 + 8 + 1 + 6; private register*Endpoints( 16 new control security-routes.ts: this.routeManager.register( 6; registrar list length 7; enforceAuth 84 → 92 re-anchored (five call sites moved and are counted in both readings; the eight new occurrences are the seam twice; no call site added); populationRule names the new module

Each number above was also counted statically on the tree (grep -oF per file).

Proof of move-only

  • .d.ts: packages/rest/dist/index.d.ts and index.d.cts built at base a515abdd8c (this worktree, before any edit), at 96504dab66 and at the merge head a12cdcf4e4 are byte-identical: sha256 7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9 for all six files, cmp silent. The head bundles carry a head-only marker (readSecurityServiceProvider, 4 hits in each of index.js and index.cjs), so the declaration was rebuilt from the moved source, not replayed. SecurityExplainRouteHost, SecurityRouteHost and security-routes appear 0 times in the .d.ts; src/index.ts is untouched.
  • Export lists: the base and head ESM and CJS bundles each load with 39 exports and one sorted export-name list (sha256 b3246be6a171… on all four), and none of the two functions or two host types is among them.
  • Routes: a source RestServer on a recording mock server, with a protocol that answers every member, dumped under four configs at base and at head: getRoutes() (functions as arity), the ordered server registration calls, and a whitespace-free sha256 of every handler's source. 12 dumps of 12 are byte-identical, at 96504dab66 and again at the merge head. default: routes 6931d1a5d182…, calls ccb3bd12f10b…, handlers 6d4926ef1465… (75 routes, 6 security); scoped-optional: 275c72f46c30… / 339e8c3abc05… / 92790e91927d… (150, 12); scoped-required: a2aa8183e213… / 734f33133150… / f6e3fd853aea… (75, 6); every-provider-set equals default. A second base run was identical to the first.
  • The handler digest can fail: a positive control changed one character of one moved handler (Delegable scope error: → !, through ablation-replace.mjs; anchor 1 → 0, blob 01fe3b195d → 0c2c62e946). Routes and calls were unchanged, and the handler list differed in exactly GET /api/v1/security/my-delegable-scope (and its scoped twin). Restored, blob 01fe3b195d equals HEAD.
  • Moved bodies: base rest-server.ts lines 10309-10600 and 10695-10837, one 4-space indent removed, against security-routes.ts lines 110-401 and 411-553: diff exits 0 both times (292 + 143 lines). No moved line has an odd number of backticks, so no template literal spans the cut and re-indenting changes no runtime string.
  • Moved-line accounting: git diff --color-moved=plain --color-moved-ws=allow-indentation-change base → move commit over the two files: 442 deleted and 470 added lines detected as moved. rest-server.ts is +43 / −449: the two stub bodies (host literal plus .call), the slice-6 import and its comment, and four import-pointer comments.

Readers re-pointed (sums unchanged, no gate loosened)

  • scripts/check-route-envelope.mjs (a gate's target paths, domain:spec): security-routes.ts is discovered by the *-routes.ts convention and declared dialectOnly with ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the five earlier modules are, at its measured stringError 0, siblingCode 0. Every arm of both families emits the nested { error: { code, message } } through its family's one emitter (the emitters [finding] registerSecurityEndpoints answers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981 and [finding] the two registrars ADJACENT to #7981 still answer the retired flat / bare-string error dialects (ADR-0112) #8073 introduced). So rest-server.ts stays at stringError 17, siblingCode 17, and the sums 17 + 13 + 8 + 1 + 0 + 0 + 0 = 39 and 17 + 15 + 8 + 9 + 4 + 1 + 0 = 54 are unchanged. Output differs from base only in the new row (16 → 17 modules, 7 → 8 ratcheted). --self-test green.
  • scripts/adr-anchors/packages__rest__src__security-routes.ts.json (new, ADR-0090 + ADR-0105 + ADR-0112): check-adr-anchors decision-letter citations on the anchored surface 1761 (base) → 1751 (move) → 1761 (head); anchored files 67 → 68. Every listed id appears in the module, and every cited letter is a decision of its record (the gate's own check, green). The invariant writes "its D6", never "ADR-0090 D6", so it adds no citation of its own (measured with the gate's regex: 0).
  • The nine registrar-list readers above.
  • error-response.ts, the nestedRefusalEnvelope docblock (rest lane, not in the published .d.ts): it named rest-server.ts as the home of three families, the suggested-binding one among them. Since slices 3, 4 and this one moved all three, the sentence now names each family's current home.

Red before the re-points, each measured with the move in place (d6fc91a97d): the six local list readers read Test Files 6 failed (6), Tests 7 failed | 103 passed (110) (five length pins, plus the docblock pin's length pin and its new module with no floor). rest-server-canonical-query-ast.test.ts (repo) read Tests 2 failed | 53 passed | 7 skipped (62). The dogfood pair read Tests 2 failed | 106 passed (108) (list length; the census row's registrar list length control). tsc --noEmit was clean, but check:test-typecheck found 2 type errors, the two typed floor tables without the new module. check:route-envelope read security-routes.ts NOT DECLARED (MODULES and EXPRESS_RESPONSE_MODULES). check-adr-anchors was green at 1751.

Readers examined and not re-pointed

  • packages/spec readers: none reads a moved line by path. No liveness anchor names either registrar or a symbol of the moved bodies. packages/spec/liveness/doc.json's note names this.registerSecurityEndpoints(bp), a call site that stays, as dated history. The packages/spec pointers slices 1–4 carried to the CRUD-doors slice are unchanged by this slice.
  • objectql-slot-consumer-census, auth-gate-allowlist-fault-window, execctx-authz-input-seam-reachability, runtime meta-list-projection-parity, core authz-store-unavailable and check-single-authz-resolver's DELEGATORS row: they read members or blocks of rest-server.ts the moved bodies do not touch.
  • No rest-server.ts#symbol anchor in the tree names either registrar or a symbol only the moved ranges hold. No rest-server.ts:NNNN line citation falls in or after the moved ranges (the highest is :5872). docs/qa/platform-checklist has no anchor on these doors.
  • Gate outputs, base a515abdd8c vs merge head a12cdcf4e4, identical: check-system-context-census, check-single-authz-resolver, check-rest-log-declared, check:test-source-alias, check:rest-log-spy-declared, check:platform-checklist, check-filter-alias-parity, check:liveness, check:cross-package-test-inputs. Identical except the scanned-file count: check-meta-type-normalized (+1, the new module), check-dispatcher-error-vocabulary (+1; 57 sites, 57 classified) and check:error-status-conformance (+2, the new module and one file from main; 463 producer sites). check-undeclared-dep-imports +2 specifiers and 1 file (the module's package imports). check-scripts-symbol-anchors +4 file-level anchors (the gate's new comments). check-affected-docs 24 → 25 route sources. The other differences come from main's two merged commits (check-durability-degradation-log-level: one objectql/src/plugin.ts line shift; check:nul-bytes and check:doc-authoring: new files).

Acceptance notes

  • rest-server.ts's resolveSecurityService docblock (about :9506) says it "Mirrors the resolver in registerSecurityExplainEndpoints". That docblock ships in the published .d.ts, so editing it would break the byte-identity this slice must keep. It is not edited, and the name still resolves to the function of that name in security-routes.ts, which holds the resolver.
  • Other-lane prose that names rest-server.ts for the suggested-binding route, read by nothing: packages/core/src/security/audience-binding-suggestion-status.ts (about :10, and about :18, which says rest-server.ts imports the vocabulary; the importer is now security-routes.ts) and packages/runtime/src/domains/security.ts (about :144). Not edited (承接者:无).
  • docs/adr/0112-…'s Consumers line names rest-server.ts for @objectstack/rest as a whole, not as these doors' home. It is a governed surface and is not edited.
  • scripts/check-route-envelope.mjs's count-history comments for rest-server.ts (the [finding] registerSecurityEndpoints answers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981 and [finding] the two registrars ADJACENT to #7981 still answer the retired flat / bare-string error dialects (ADR-0112) #8073 entries) say the two registrars sat in that file when those counts moved. They are dated history, unchanged.
  • docs/adr/0076-… (status line and D11) and docs/adr/0111-… (Consumers line) are still carried to the maintainer by the seat; this slice adds nothing to them.

Gates and tests

Measured at this branch: base a515abdd8c, the move d6fc91a97d, the re-points 96504dab66, and one merge of origin/main f40d877f81 (a12cdcf4e4, two commits in objectql, plugins/organizations and one dogfood test, no file in this diff). All builds and tests went through os-verify-lock. Every exit code was captured to a file before any pipe.

  • At a12cdcf4e4: pnpm install --frozen-lockfile exit 0. pnpm turbo run build --concurrency=2, the whole workspace, exit 0 (76 tasks, 33 cached).
  • pnpm --filter @objectstack/rest typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: 0 file(s) / 0 error(s)), at 96504dab66 and at a12cdcf4e4.
  • pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 285 passed (285), Tests 5870 passed / 326 skipped (6196), at both heads.
  • pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: Test Files 5 passed (5), Tests 234 passed / 7 skipped (241), at both heads.
  • pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts: Test Files 2 passed (2), Tests 108 passed (108), at both heads.
  • dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at the final head a12cdcf4e4 (stderr names that commit; the --repo assertion holds): 79 commands, all run at a12cdcf4e4, all exit 0. --ran: Run reconciliation — 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN. Among them are pnpm check:route-envelope (its --self-test, then the scan), pnpm check:adr-anchors, pnpm check:durability-log-level, pnpm check:dispatcher-error-vocabulary, pnpm check:dts-closure, pnpm check:dual-build-cjs-loads and pnpm check:pm-dispatch-gates.
  • Lint, the proven narrowing: eslint --no-inline-config --format json over packages/rest, the two dogfood files, scripts/check-route-envelope.mjs and the new anchor JSON: 330 files, 0 errors, 0 lint warnings. Its one warning is File ignored because no matching configuration was supplied for the JSON, which eslint . never reads. All 15 changed files are in the result set at 0 / 0. The published .d.ts is byte-identical, so no consumer outside packages/rest can lint differently. Repo-wide pnpm lint is CI's.
  • check:dev-prereqs and check:skill-frame-freshness (local-only) were not run: no packages/spec build input and no four-axis frame file is touched.
  • No changeset: nothing any released package publishes changes (.d.ts, export lists and routes byte-identical above). skip-changeset is the seat's to apply on its own check.

Cross-lane files in this diff: scripts/check-route-envelope.mjs (domain:spec, a gate's target paths), scripts/adr-anchors/packages__rest__src__security-routes.ts.json (a new entry in check-adr-anchors' registry, domain:devx), and the dogfood pair (packages/qa/dogfood/test/authz-conformance.test.ts, authz-probe-blind-spot.census.ts). No platform-checklist file is edited.


Generated by Claude Code

…utes.ts (#23050 slice 6)

The bodies of RestServer#registerSecurityExplainEndpoints (GET/POST
{basePath}/security/explain, GET {basePath}/security/my-delegable-scope)
and RestServer#registerSecurityEndpoints (GET {basePath}/security/
suggested-bindings and its /:id/confirm and /:id/dismiss) move, unedited
and re-indented one level, to ONE new module. The class keeps both
private methods and their docblocks as host literals plus .call;
routeManager goes by value, kernelManager and securityServiceProvider as
getters. The imports only these bodies read move with them, each leaving
a pointer comment. The registrar list gains security-routes.ts (6 -> 7).

Move-only commit: the readers are re-pointed in the next commit.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…23050 slice 6)

Each reader whose reading included a moved line now reads it in
security-routes.ts, with its sum unchanged:

- the nine registrar-list readers re-read their numbers at length 7
  (query-number 9, canonical query slots 4, meta-draft switches 8,
  docblock floors 29, execctx 62 sites / 109 mentions with eight
  forwarders = (7 - 1) + 2, ui-view pair 55 / 70, the dogfood tripwire 0,
  the blind-spot row 70 / 19 / 51 with register 63 and enforceAuth 92);
- check-route-envelope declares security-routes.ts dialectOnly at
  stringError 0 / siblingCode 0 (rest-server.ts unchanged at 17 / 17);
- a new ADR anchor for security-routes.ts (ADR-0090, ADR-0105, ADR-0112)
  restores check-adr-anchors' decision-letter reading 1751 -> 1761;
- error-response.ts' nestedRefusalEnvelope docblock names the three
  families' current homes.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 37 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/error-response.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

45 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 9bb93706fbdf57582329745b64c2f40c6a084572.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/error-response.ts) — pages documenting those are invisible to this run
  • 3 cross-cutting symbol(s) contributed no route anchor: enforceAuth (23 routes), resolveExecCtx (28 routes), routeManager (20 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 36 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 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 9bb93706fbdf57582329745b64c2f40c6a084572 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fd8cd453e82d6ad29fabb36a694278f87ba9ff9f — the merge of head a12cdcf4e4a5f2477dd537b451bcd7ceae37c1ff into base 9bb93706fbdf57582329745b64c2f40c6a084572, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fd8cd453e82d6ad29fabb36a694278f87ba9ff9f && git checkout fd8cd453e82d6ad29fabb36a694278f87ba9ff9f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9bb93706fbdf57582329745b64c2f40c6a084572 a12cdcf4e4a5f2477dd537b451bcd7ceae37c1ff && git checkout -B drift-repro 9bb93706fbdf57582329745b64c2f40c6a084572 && git merge --no-ff a12cdcf4e4a5f2477dd537b451bcd7ceae37c1ff

node scripts/docs-audit/affected-docs.mjs --json 9bb93706fbdf57582329745b64c2f40c6a084572

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9bb93706fbdf57582329745b64c2f40c6a084572 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: a12cdcf4e4a5f2477dd537b451bcd7ceae37c1ff
Local-runs: none

Face: the declaration face. This is a move-only refactor of @objectstack/rest: two registrar bodies move into one new module, one gate's target paths are re-pointed (scripts/check-route-envelope.mjs), one entry is added to a gate's registry (scripts/adr-anchors/), and the registrar list that nine readers read grows 6 → 7. The 15-file list holds no published schema and no governed path. record-recognisers --template --paths over it derives TIER_DEFAULT, and Governed Surface Queue Guard concluded success.

Inputs:

The reviewer is an isolated agent working read-only. Every count below was read off the git objects of the merge-base and the head with git show, text diff, grep, and check-adr-anchors' own DECISION_CITATION pattern applied to text. Nothing was built, tested or re-run. The merge-base's rest-server.ts blob (d5ddbc9d00) is the same blob as at the dev's base a515abdd8c, so the PR's line numbers apply as written.

① Derived judgments

No accept set and no public surface changes. Each change the diff makes is judged below.

  1. The moved bodies are byte-identical: right.

    • Base rest-server.ts :10309–:10600 and :10695–:10837, with one 4-space indent removed, equal head security-routes.ts :110–:401 and :411–:553 (292 + 143 lines). The text diff is empty both times.
    • Every non-blank line in those ranges carries the indent. No moved line has an odd number of backticks, so no template literal spans the cut and re-indenting changes no runtime string.
    • Each body's local helpers moved inside it: the explain body's resolveService, respondError and delegableHandler, and the suggested-binding body's resolveService, respondError, respond501 and handleError. The explain body's dynamic import('@objectstack/spec/security') moved with it.
    • Both private methods keep their name, their docblock (unchanged context lines) and their position. The call sites this.registerSecurityEndpoints(bp) and this.registerSecurityExplainEndpoints(bp) are unchanged (base :4812–:4813, head :4814–:4815), so the six routes register in the same order.
    • The rest of the rest-server.ts side is the two stubs, the one new import and four pointer comments. Its top-level export lines are identical at base and head.
  2. Each host seam lists exactly what its body reads: right.

    • The explain body's this. reads are routeManager 3, kernelManager 2, securityServiceProvider 2, resolveRequestEnvironmentId 1, resolveExecCtx 2 and enforceAuth 2.
    • The suggested-binding body's are routeManager 3, securityServiceProvider 2, resolveExecCtx 3 and enforceAuth 3.
    • Neither body has any other this outside comments. Its only function tokens are inside typeof string comparisons.
    • SecurityExplainRouteHost (6 members) and SecurityRouteHost (4) declare exactly those, each as an indexed access on RestServer. Each host is an object literal of its type, so a missing or extra member is a compile error. TypeScript Type Check and the four Type Check · lanes concluded success.
  3. Value or getter, member by member: right.

    • routeManager goes by value in both literals.
      • All six register calls are top-level statements of the two bodies, so they run during registration.
      • The field is reassigned only at :5721 / :5725, a try/finally around registerMetadataEndpointsInner. registerRoutes calls both security registrars outside it.
    • kernelManager (explain only) and securityServiceProvider (both) go as getters, in each literal whose body reads them.
      • Each is read only inside its body's resolveService, which the handlers call at request time.
      • Both fields are private and not readonly (:2021, :2091), and only the constructor assigns them (:2158, :2171).
      • This is slice 2's refinement (6189011700). The getters read through closures over the server, never the host.
      • The dev's by-value ablations (the ablated door reads 501 then 501, not 501 then 200) are theirs, not re-run, and they match this reading.
    • Methods go as late-bound closures that call the method on the server.
    • One residual difference, judged harmless as in slices 2–5: securityServiceProvider is now called with the host as its receiver. Its declared type is a plain function type, and the one shipped construction (rest-api-plugin.ts :480) is an arrow function. kernelManager.getOrCreate is still called on the manager itself.
  4. The seven imports moved as a pure move: right.

    • Base code uses of the seven names, all inside the two bodies:
      • isAudienceBindingSuggestionStatus and unknownAudienceBindingSuggestionStatusMessage: :10784 and :10790, in the suggested-binding body;
      • standardErrorCodeForHttpStatus: :10514, in the explain body (:7282 and :7502 are comments);
      • ErrorCode: :10357 and :10513, in the explain body;
      • classifiedRefusalAnswer, nestedRefusalEnvelope and sandboxFaultAnswer: in both bodies and nowhere else.
    • At head, every remaining mention of the seven in rest-server.ts is a comment.
    • sendError as sendEnvelopeError, logError and refuseRepeatedQueryParams still have code uses in rest-server.ts, so they stay. The module imports them from the same specifiers.
    • The module imports only type RestServer from ./rest-server.js. That is no runtime cycle, and no export was added to rest-server.ts.
    • The four re-worded pointer comments in rest-server.ts and the re-worded [#11683] comment in the module are accurate to those uses. respondSharingError in sharing-routes.ts does use standardErrorCodeForHttpStatus (:328).
  5. The published surface is unchanged: right.

    • @objectstack/rest exports . only.
    • src/index.ts is byte-identical at base and head. It re-exports from ./rest-server.js by name only, and it names neither security-routes.ts nor error-response.ts.
    • The host types appear only inside private method bodies, which declaration emit does not reach.
    • The dev's .d.ts, export-list and route-dump readings are theirs, not re-run here. They are what this construction predicts.
  6. scripts/check-route-envelope.mjs followed the move and was not loosened. That is right, apart from one wrong word in a comment.

  7. The new ADR anchor is right, needed, and in proportion.

    • Why it is needed, measured with the gate's DECISION_CITATION pattern:
      • the module carries 10 ADR-NNNN Dk citations: ADR-0090 D5 ×2, D6 ×2 and D12 ×2; ADR-0105 D8 ×2; ADR-0112 D5 ×2;
      • rest-server.ts goes from 104 to 94.
      • The move took exactly those 10 off the anchored rest-server.ts (anchored for ADR-0045). That is the silent narrowing the card's dead-pin ⛔ forbids, and this entry puts them back under the letter check.
      • Slice 4 did the same for sharing-routes.ts, and its record judged it right. Slice 5 owed none, because its body carried 0.
    • The listed ids: each appears in the module (ADR-0090, ADR-0105, ADR-0112). Every cited letter is a decision of its record: ADR-0090's D5, D6, D9 and D12 are headings, and ADR-0105's D8 and ADR-0112's D5 are bold-lead paragraphs.
    • The invariant adds no citation: it writes "its D6", never "ADR-0090 D6", so the gate's pattern finds 0 in it.
    • Its letters, checked here by hand instead:
      • ADR-0090 D6 is the explain engine, and D12 is delegated administration;
      • ADR-0090 D5 and D9 are "packages suggest, admins confirm", never auto-bound;
      • ADR-0105 D8 is scoped invitations;
      • ADR-0112 D5 is the nested error.code.
      • Each attribution matches its record and the moved code's own citations.
    • Note, not a defect: "every refusal of each family leaves through that family's ONE emitter" holds for the families' own arms. Two answers come from elsewhere: the shared enforceAuth seam's anonymous 401 (flat ANONYMOUS_DENY_BODY) and the repeated-query helper. The bodies' own docblocks make the same family-scoped claim, and slice 4's anchor is worded the same way.
    • It is a registry entry of an existing gate, not a new gate, so the card's "No new gate" holds.
    • Verdict: the ADR anchors + number uniqueness step concluded success in Lint · repo & tooling gates.
  8. The registrar list and its readers: nothing loosened, right. The list grows 6 → 7 by an appended entry. Each reader was recounted at head:

    • execctx census:
      • resolveExecCtx mentions go 101 → 109 (57 + 12 + 7 + 9 + 12 + 3 + 9).
      • rest-server.ts goes 58 → 57: it loses 5 sites and gains 4 forwarder mentions. The module holds the 5 sites plus 4 interface-member mentions.
      • The 62 sites stay pinned exactly (27 + 9 + 5 + 7 + 8 + 1 + 5).
      • There are 8 forwarder lines = (7 − 1) + EXTRA_MOVED_REGISTRARS 2. The map's new key security-routes.ts: 1 is the second module carrying two registrars, counted as slice 4's was, and the total is pinned at exactly 2.
      • Same population, re-counted.
    • docblock pin:
      • The module holds exactly 8 blocks: the header, two interfaces, two functions, the explain family's respondError and delegableHandler, and the suggested-binding family's respondError. Its floor sits at its actual count.
      • rest-server.ts goes 167 → 164 blocks, against MIN_BLOCKS 100.
      • The floors' sum goes 21 → 29: the new module's 8 added to an unchanged 21. That sum is pinned.
    • ui-view pair: this.enforceAuth( goes 53 → 55 and this.resolveExecCtx( 68 → 70, from the two forwarders. The floors of 40 are unchanged.
    • query-number 9, canonical-query 4, meta-draft 8: the module has 0 numeric coercions, 0 query: literals and 0 draft tokens. Each sum is unchanged, and the floor is security-routes.ts: 0.
    • dogfood blind-spot row:
      • this.routeManager.register( is 63 on both sides. At head that is 27 + 9 + 5 + 7 + 8 + 1 + 6: rest-server.ts went 33 → 27, and the module holds 6 (3 + 3).
      • private register*Endpoints( is 16 on both sides.
      • enforceAuth goes 84 → 92 (41 + 11 + 7 + 9 + 12 + 3 + 9). rest-server.ts goes 42 → 41 (5 sites out, 4 forwarder mentions in), and the module holds 9 (5 sites and 4 interface members). No call site was added: the control is re-anchored, not relaxed.
      • The population of 70 / 19 / 51 is unchanged.
    • dogfood realtime tripwire: neither probe's tokens appear in the module (0 hits). The length is pinned at 7.
  9. error-response.ts: comment only, and accurate.

    • The nestedRefusalEnvelope docblock now gives the homes of the sharing-rule, suggested-binding and approval families as sharing-routes.ts, security-routes.ts and approvals-routes.ts (slices 4, 6 and 3).
    • Those modules hold 3, 3 and 2 nestedRefusalEnvelope( calls, and rest-server.ts holds none.
    • The package entry cannot reach this module.
  10. Sweep of the other readers for a missed re-point: right.

    • All ten files that import the registrar list are in the diff.
    • No rest-server.ts# anchor in the tree or in the platform checklist names either registrar. The checklist anchors name stay-put symbols such as #registerFormEndpoints, #registerCrudEndpoints and #registerMetadataEndpointsInner.
    • The behavioural envelope tests for these doors boot RestServer and read no source. Test Core (1–6 and the aggregate) concluded success.

② Semver level

  • skip-changeset is right. Nothing any released package publishes changes.
    • For @objectstack/rest, the .d.ts, the export list and the routes hold by construction (① items 1, 2 and 5).
    • The rest is tests, @objectstack/dogfood (which is private), comments, one gate script and one gate-registry entry.
    • A move that changes no behaviour is not a feature, an improvement or a fix, so no patch changeset is owed.
  • Clause-②: no is right. No accept set widens or narrows.
    • The closed query set refuseRepeatedQueryParams(req, res, ['status', 'packageId']) and the ?status= vocabulary check travel unedited.
    • No route is added or removed, and no spec key moves.
  • The changeset check's history:
    • Check Changeset concluded failure at 22:01:02Z, before the label.
    • skip-changeset was written at 22:05:17Z by objectstack-fleet[bot], the seat's write path, as the claim says.
    • The re-run concluded skipped at 22:05:20Z.

③ Boundary flags

Every dev flag is answered or escalated. The flags come from the PR's acceptance notes and from the report's (6203179631) open_questions, deviations and out_of_scope_findings.

  1. open_questions is empty. There is nothing to answer.

  2. The siblingCode comment's "two arms" (① item 6). Not blocking. ESCALATED to the seat, which has two options:

    • amend "two" to "four" before landing. That moves the head, so this record would no longer govern it.
    • carry the fix to the next slice that appends to the same history block.

    Landing as-is leaves one wrong word in a dated comment.

  3. Deviation: the new ADR anchor. Answered: right, needed and in proportion (① item 7). It is cross-lane (domain:devx); the seat's declaration on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023 is in the PR timeline at 22:04Z.

  4. Deviation: comment wording beside the moved imports, and the module's new pointer comment at its sendError import. Answered: accurate (① item 4).

  5. Deviation: error-response.ts's sentence re-worded for all three families. Answered: accurate, and in scope as the re-point of a reader that names the moved family (① item 9).

  6. Deviation and acceptance note: resolveSecurityService's "Mirrors the resolver in registerSecurityExplainEndpoints" was not edited. Answered: right. That docblock ships in the .d.ts, and the name still resolves to a function of that name that holds the resolver.

  7. Moved-text deixis, found here and not flagged by the dev. The explain emitter's docblock, moved verbatim, says finding: rest-server.ts 里三个相邻 /meta handler 的错误信封是三种不同形状,其中两种不符合 ADR-0112 #7035 removed both dialects "from this file's /meta refusals". In security-routes.ts, "this file" has no /meta.

    • A move-only slice may not edit the body, and the module's header points back at rest-server.ts's import note.
    • Note only, 承接者:无. The first non-move edit to that docblock can fix it.
  8. Deviation: one merge of origin/main (f40d877f81). It is disjoint from the diff.

  9. Deviations: the eslint lock reading, the trailers, the stray root pid files and the worktree cleanup. None is a contract matter.

  10. out_of_scope_findings:

    • other-lane prose that names rest-server.ts for the suggested-binding route: audience-binding-suggestion-status.ts :10 / :18, and runtime's domains/security.ts :144;
    • the governed ADR lines (0076, 0111 and 0112).

    Answered: leaving them is right. The ADR lines stay with the seat's Tier H carry, and this slice adds nothing to them.

  11. Cross-lane files: check-route-envelope.mjs (domain:spec, declared on [PM seat] domain:spec — 🟢 os-elon-musk · session_01HjR8RUwTPQ3TgbfKihmxp9 #6017 at 22:04Z), the anchor (domain:devx, [PM seat] domain:devx @ objectstack — ⏳ vacant #6023) and the dogfood pair. That is process, not a contract defect.

  12. The dogfood pair has no PR-time verdict. Since skills(ci): two-speed CI — fast set on PR and merge-queue runs, full battery hourly on main and before a release, main-red stops the lane, four required contexts plus the guard, timing gates report-only, test budgets not deletions #22998, the shards run only in main's full battery.

    • On this head, the Dogfood Regression Gate matrix leg and Dogfood Verify CLI skipped, and Dogfood Regression Gate concluded success.
    • The edits are attested by the dev's local run (108 passed) and by the static recount in ① item 8, which matches every pinned number.
    • Answered: not blocking, by the repo's own CI design. The seat watches the first full battery after landing, as it did for slices 4 and 5.
  13. Tier: this file list derives TIER_DEFAULT, and that is the tier this record serves. The claim promised a review "at the tier the PR's face maps to".

  14. PR shape:

    • The PR is a draft with base main.
    • Line 1 is Part of #23050 and line 2 is Clause-②: no. The body has no closing keyword.
    • The assignee is sunhaolin, and the labels are size/xl, tests and skip-changeset. There is no needs:contract-review label to remove.
  15. Gate verdicts on this head. All 45 runs had completed when read. Taking the latest run per name (41 names) gives 32 success, 9 skipped and 0 failure. The one superseded failure is the pre-label Check Changeset (② above). The successes include:

    • Build Core, Test Core (1–6 and the aggregate) and TypeScript Type Check;
    • the four Type Check · lanes, Lint & Repo Gates and all eight Lint · lanes;
    • Governed Surface Queue Guard, Spec property liveness, Part-of PR must not also close its card and No other open PR may claim the same single-writer path.

Implemented-by: claude/issue-23050-s6-security-endpoints
Reviewed-by: session_01Q9PZ5Pzqcus4woVvTZwWNU

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

✅ ACCEPT: PR #23729 at a12cdcf4e4 (#23050 slice 6), round 0. Every check on this head is green or a roster skip. The isolated review record is 6203715861 (PASS) · 2026-10-15T22:38Z

domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6201888913. Report: the os-dev-report 6203179631 on #23050.

Checklist, read against GitHub and origin/main, not against the report:

  • PR shape: draft, base main, title refactor(rest):, assignee sunhaolin. Line 1 is Part of #23050: the card stays open for later slices. Line 2 is Clause-②: no.
  • Scope: 15 files, +721 / −469.
    • The move: rest-server.ts loses the bodies of registerSecurityExplainEndpoints (292 lines: GET / POST {basePath}/security/explain, GET {basePath}/security/my-delegable-scope) and registerSecurityEndpoints (143 lines: GET {basePath}/security/suggested-bindings and its confirm / dismiss) to the one new security-routes.ts, and keeps two host-literal stubs. 11,042 → 10,636 lines.
    • The registrar list grows to 7.
    • Every reader whose reading includes a moved line is re-pointed, with its sum unchanged.
    • check-governed-merges --pr 23729: NOT governed. git merge-tree is clean against origin/main 9bb93706fb, and no file main changed since the merge base is in this diff.
  • Move-only, proven:
    • Both moved bodies are byte-identical after de-indentation (diff exit 0, 292 + 143 lines).
    • getRoutes(), the ordered registrations and every handler's hash are 12/12 identical under 4 configurations, with a positive control (a one-character edit is caught in exactly its door).
    • The published .d.ts / .d.cts are byte-identical (sha256 7489de84…), and both bundles' export lists are unchanged (39 each). Nothing new is exported.
    • routeManager goes by value. kernelManager (explain host) and securityServiceProvider (both hosts) go as getters, measured: 501 then 200 after a post-registration assignment, and each by-value ablation stays 501.
    • The seven imports only the bodies read moved with them. No helper was copied or re-derived.
  • Changeset: none. This seat applied skip-changeset on its own check: no .changeset file is touched, and the published surface is byte-identical.
  • Gates:
    • Local, at a12cdcf4e4: 79 derived, 79 run, 0 unmeasured, all exit 0. The @objectstack/rest suite: 285 files, 5870 passed (repo project: 234 passed). The typecheck exits 0. The dogfood pair: 108 passed.
    • Gate evidence: the check-runs pinned to a12cdcf4e4, read at this stamp. 41 distinct checks: 32 success, including Lint · ESLint, the Type Check family, all six Test Core shards, Build Core and Governed Surface Queue Guard, and 9 skipped.
      • 8 of the skips are roster names: Auto Label, Build Docs, Check PR Size, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance.
      • The ninth is Check Changeset, which skipped on its re-run after the skip-changeset label (its first run, before the label, was the expected red).
      • Nothing is running and nothing failed.
  • The review: isolated, served at TIER_DEFAULT (the tier record-recognisers.mjs --template --paths derives for this file list), record 6203715861, PASS.

Cross-lane, declared before review:

  • scripts/check-route-envelope.mjs on domain:spec's post (6203227319): the new module declared at 0 / 0, rest-server.ts unchanged at 17 / 17, the sums 39 and 54 held.
  • The new scripts/adr-anchors/packages__rest__src__security-routes.ts.json on domain:devx's post (6203235741): hypothesis (c) held, so the decision-letter reading, which fell 1761 → 1751 while green, is restored to 1761.
  • The dogfood pair is this lane's. No checklist file is touched.

Accepted deviations:

  • the new ADR anchor, as the dispatch foresaw for hypothesis (c);
  • re-worded comments beside the moved imports, and error-response.ts' nestedRefusalEnvelope sentence naming the three families' current homes (comments only, not in the .d.ts);
  • rest-server.ts' resolveSecurityService docblock left naming registerSecurityExplainEndpoints, because it ships in the .d.ts; the name still resolves;
  • origin/main f40d877f81 merged once before the PR.

The review's one non-blocking item, carried rather than fixed here: the new siblingCode history comment in check-route-envelope.mjs says "the two arms #7981 and #8073 converted … 77 → 73". It is four arms, two per PR; every number in it is right. Fixing it now would move the head off the reviewed and green a12cdcf4e4 for one comment word, so it is carried to #23050's next slice that edits that comment block (named in this card's release). The moved explain emitter docblock's "this file's /meta refusals" is carried the same way, as a move-only slice cannot edit a moved body.

Out of scope, 「承接者:无」 (named, not edited): other-lane prose naming rest-server.ts for the suggested-binding door, read by nothing: packages/core/src/security/audience-binding-suggestion-status.ts (about :10, :18) and packages/runtime/src/domains/security.ts (about :144).

Landing: pr_ready + automerge_enable through the relay, in this act. On landing, #23050 is released to pm:queue for its next slice. This seat watches main's first full battery for the dogfood pair, as for slices 4 and 5.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 22:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 22:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit d0f7eb5 Oct 15, 2026
45 of 46 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23050-s6-security-endpoints branch October 15, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl 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