Repository navigation
fix(plugin-auth, runtime): four principal-less producers take the explicit system opt-in - #21939
Conversation
… explicit system opt-in The platform-admin OAuth client toggle route reads and writes sys_oauth_application through withSystemContext, the wrapper better-auth's adapter already writes those rows through. The SCIM bearer verifier's credential probe and the dispatcher's environment-membership read pass isSystem: true in the read's trailing options. Each call is authorized by its own door (the platform-admin judge, the bearer digest, the membership gate itself); none now reaches the engine as a context with no principal and no opt-in, the security middleware's principal-less hand-off (ADR-0096). No door, answer or stored row moves. Pins: a context recorder on a real engine (plugin-auth) and a find double (runtime), plus the membership gate's answers on an engine that refuses a principal-less context and its unchanged fail-open catch. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…r the three system opt-ins The census page's held declaration count follows the new trailing-options type on the SCIM credential probe (check-system-context-census --fix). patch changesets for plugin-auth and runtime. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…incipal-less-producers-identity
…it system opt-in beforeUpdateOrganization reads sys_organization and sys_environment through withSystemContext. The hook is the slug guard itself: the organization id is the where, not the reader, so neither read reaches the engine as a context with no principal and no opt-in (ADR-0096's principal-less hand-off). The catches around both reads are unchanged. Pins: both reads carry the opt-in; the guard still refuses on an engine that refuses a principal-less context; the pre-existing catches still end the hook without refusing. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
… that take the system opt-in Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…incipal-less-producers-identity
…uery, not the trailing options Same opt-in, same engine reading (ObjectQL merges the query's context with the trailing one). Spelled inside the query so the existing membership suites, which assert the read's two arguments, stay unchanged. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 36 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9378e862d9522e8d4b3b335b064f391cbfa7dde0 && git checkout 9378e862d9522e8d4b3b335b064f391cbfa7dde0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9dce635337c2cc42a4149aa49289ad77d172363d 9878b925fc207120aae9966a26929a58d274c268 && git checkout -B drift-repro 9dce635337c2cc42a4149aa49289ad77d172363d && git merge --no-ff 9878b925fc207120aae9966a26929a58d274c268
node scripts/docs-audit/affected-docs.mjs --json 9dce635337c2cc42a4149aa49289ad77d172363d
|
…cision in words instead of a tracker number (stage 23) (objectstack-ai#21947) Part of objectstack-ai#20749 Clause-②: no Stage 23 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the last name-ordered `ui/` group: the 16 id-bearing test files directly under `packages/spec/src/ui/` from `view-item-config-type.test.ts` to `widget.test.ts`. Those files carried 100 messages and 106 tracker ids, citing 53 records. All 106 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed. ## Census at the base (`9e33ee7c59`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages 10 to 22 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The worktree was cut from `origin/main` at `9e33ee7c59`, the claim's base. Both instruments read **571 messages / 604 ids in 127 files**, the seat's reading and stage 22's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ui/` (this PR: 16 of the 21 files) | 21 | 107 / 113 | 97 / 103 | 10 / 10 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **127** | **571 / 604** | **523 / 553** | **48 / 51** | The group reads **100 messages / 106 ids in 16 files**, the seat's figures file for file: | file (under `ui/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `view-item-config-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-metadata-schema.test.ts` | 8 / 8 | 8 / 8 | 0 | | `view-metadata-type.test.ts` | 2 / 2 | 2 / 2 | 0 | | `view-overlay-options-bag.test.ts` | 6 / 6 | 6 / 6 | 0 | | `view-overlay-options-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-overlay-owner-hidden-retirement.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-overlay-viewkind-arm.test.ts` | 10 / 10 | 10 / 10 | 0 | | `view-overlay-viewkind-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-strictness-batch18.test.ts` | 10 / 11 | 10 / 11 | 0 | | `view-submit-redirect-url.test.ts` | 5 / 5 | 5 / 5 | 0 | | `view-union-branch-focus.test.ts` | 7 / 7 | 6 / 6 | 1 / 1 | | `view-union-diagnostics.test.ts` | 4 / 5 | 4 / 5 | 0 | | `view-union-retirement-prescription.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view.test.ts` | 39 / 43 | 38 / 42 | 1 / 1 | | `widget-i18n-retirement.test.ts` | 3 / 3 | 2 / 2 | 1 / 1 | | `widget.test.ts` | 1 / 1 | 1 / 1 | 0 | | **16 files** | **100 / 106** | **97 / 103** | **3 / 3** | Three more test files sit in the same name range and carry no id (`view-item-owner-hidden-retirement.test.ts`, `view-list-tabs-retirement.test.ts`, `vocabulary-derivation.test.ts`). The three "other" strings are rewritten and declared to the text-only tool: the table label at `view-union-branch-focus.test.ts:139`, which prints inside two `for … of` test titles, and the expect messages at `view.test.ts:4099` and `widget-i18n-retirement.test.ts:135`. - **Controls.** Lit: `ui/notification.test.ts` and `api/api-error-code-type.test.ts`, outside the group, read 1 id each at the base and at the head. Dark: `view.test.ts` reads 0 at the head while 92 of its comment lines still carry a number. Planted in a scratch tree: an id put into a `widget.test.ts` title reads 1 / 1 (`title:describe`), and an id put into a `view-metadata-type.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in 15 of the 16 files at the base. `view.test.ts` reads 2 more, and keeps them at the head: the CSS colours `'#00cc00'` (`:2770`) and `'#22c55e'` (`:3537`), fixture values that cite nothing. - **At the head:** 471 messages / 498 ids in 111 files. The 16 files read 0 / 0, `ui/` reads 7 / 7, and no other file moved. ## How the area was chosen `ui/` has no subdirectory test file with an id, so it is taken in name-ordered file groups near the ~100-id bound. Stage 22's re-cut named this group at 106 ids, and this census reads 106, so no re-cut was needed. `view.test.ts` (43 ids) is one file inside one text-only proof here, so it is not split. **`ui/` after this PR** reads 7 / 7, all kept items: stage 20's `component-props-unknown-members.pin.test.ts:322`, stage 21's four colour literals (`dashboard-chart-structure-refusal.test.ts:94`, `dashboard.test.ts:124`), and stage 22's two needles (`notification.test.ts:123`, `strictness-batch14.test.ts:395`). **Named for the next stages** (cut from the head census, 471 / 498): - **`api/`, 201 ids in 40 files**, with no subdirectory. Its first name-ordered group near the bound is `ai-agents-envelope.test.ts` through `package-lifecycle.test.ts`: 27 files, 100 messages / 106 ids (95 / 101 titles, 5 / 5 other: `auth.test.ts`, `discovery-environment-subset.pin.test.ts` and three in `export-job-family-retirement.test.ts`). The second is `plugin-rest-api.handler-status-retirement.test.ts` through `zod-issues-to-fields.test.ts`: 13 files, 89 / 95, `protocol.test.ts` alone 50. - `system/` 167, two stages. The files directly in `src/`, 120, one. - The needles: the three docblock needles, the kept `:322` and stage 22's two. One stage, with an at-tier review. ## What each id became - **25 literals (27 ids)** now state a decision in words. - **20 literals (22 ids)** get their subject back in words, where the number stood for a thing. - **55 literals (57 ids)** drop a number the title already explains. Every cited record was fetched with all its comments through REST, and its decision was read from its ruling, ACCEPT and landing comments: 53 records, 51 answer 200 and 2 answer 404. Five citations are objectui's and were read from objectui: `objectui#5233`, `objectui#2231` (cited bare at `view-strictness-batch18.test.ts:309`), `objectui#6237` (cited bare at `view.test.ts:949`), `objectui#5435` and `objectui#3289`. Three same-number records in the other repository were fetched first and set aside: `objectstack#2231` is a version-packages PR, `objectstack#6237` a datasource PR, and `objectui#2998` a form PR; `framework#1894 / objectstack-ai#2998` are this repository's objectstack-ai#1894 and objectstack-ai#2998 under its old name. The two that answer 404 were read from what landed: - **objectstack-ai#9933**, from its landing commit `d5552ca13f` ("admit columnState as an explicitly runtime-only view-overlay key") and the CHANGELOG entry for `d5552ca`; - **objectstack-ai#11195**, from PR objectstack-ai#11458, the PR that closed it ("UserActionsConfigSchema adopts group / hideFields / rowColor (ruled A on objectui#5435)"). One citation names the wrong record, and the titles now state what landed instead. `objectstack-ai#3896 close-out` (twice in `view.test.ts`) names objectstack-ai#3896, the sharing-rule criteria card, which records no decision about these keys; the two titles state the decision from the landed tombstones of `form.defaultSort` and `view.responsive` / `view.performance`, as stage 20 did for `action.test.ts`. Where a record's first decision was corrected later, the title follows the correction: - **objectstack-ai#6926:** its first triage direction retired the `groups` alias; the measurement found live consumers, and the maintainer re-ruled A, a fold at the producer. The two titles say "the producer-side `groups` fold" and "folds onto `sections`". - **objectstack-ai#7025 and objectstack-ai#7741:** objectstack-ai#7025 froze the acceptance face; objectstack-ai#7741's ruling then moved it, and later retirements moved it again, each pinned. The title says "frozen by the diagnostics work; every move since is deliberate and pinned", not "moved only once". - **objectstack-ai#7510:** the "[objectstack-ai#7510] ⛔ the acceptance face did not move" describe sits beside two ruled moves recorded in its own comments, so the title now names what did not move it: "⛔ the branch focusing did not move the acceptance face". **Stated in words:** | record | literal (under `ui/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#5599 | `view-metadata-schema.test.ts:95` | "REJECTS a bare `{}` — the pin this line used to make, reversed by the identity precondition" | Maintainer ruling 2026-08-06, direction B: a minimal identity precondition ahead of the union's four members; each member's `.strip()` is untouched. | | objectstack-ai#7741 | `view-metadata-schema.test.ts:125` | "… NO object binding — a row no read path could serve, with located guidance" | Maintainer ruling 2026-08-12, direction B: a row that cannot be expanded or served by any read path is not stored and badged valid; the inline arm requires the binding, refused with `defineView`'s guidance. | | objectstack-ai#5599 | `view-metadata-schema.test.ts:215` | "identity precondition — a body must read as a view before any member judges it" | The same direction B. | | `objectui#5233` | `view-metadata-schema.test.ts:413` | "… a `columnState`-only patch (the patch-only write the console persists)" | Maintainer ruling 2026-08-12 (on objectstack-ai#7494): `persistViewPatch` stores the patch only, not the merged base. | | objectstack-ai#17152 | `view-overlay-owner-hidden-retirement.test.ts:335` | "… names the family's D2 conversion (ruled: a D3 entry per family, even beside a lossless D2)" | Ruling B (director seat, 2026-09-10, upheld 2026-09-11): one D3 semantic entry per retired family, beside its D2 conversion even when D2 is lossless. | | objectstack-ai#7494 | `view-overlay-viewkind-arm.test.ts:102` | "the console %s toggle (a patch-only write, as ruled) is ACCEPTED on listOverlay" | Maintainer ruling 2026-08-12: the overlay store is org-wide, and the toolbar write stores the patch only. | | objectstack-ai#4001 | `view-strictness-batch18.test.ts:91` | "批 18, unknown keys refused — the doors these shapes are reachable through" | The strictness campaign: an unknown key on the authorable surface is refused, not silently stripped. | | objectstack-ai#15469 | `view-strictness-batch18.test.ts:149` | "… a CLOSED entry, and since the renderer-ahead `.passthrough()` was removed a CLOSED parent too" | Maintainer ruling A (decision batch objectstack-ai#41, 2026-09-05): every key the gantt and tree renderers read is declared, and both `.passthrough()` calls go. | | objectstack-ai#5074 | `view-strictness-batch18.test.ts:364` | "[RESOLVED by the ruled split] ViewItemSchema SPLIT — …" | Maintainer ruling A (2026-08-04): split — a strict authoring `ViewItemSchema` and a reopened wire member in the union. | | objectstack-ai#5074 | `view-strictness-batch18.test.ts:403` | "[RESOLVED with the ruled split] ListViewSchema.sort CLOSED — …" | The split's scope addendum: the wire door strips the console's decoration keys before validating, so `sort[]` closed again with no declared `id`. | | objectstack-ai#7025, objectstack-ai#7741 | `view-union-diagnostics.test.ts:246` | "the acceptance face of ViewMetadataSchema — frozen by the diagnostics work; every move since is deliberate and pinned" | objectstack-ai#7025's sweep rule: the diagnostic face improves, the acceptance face does not move; objectstack-ai#7741's ruled binding requirement is the first pinned move since. | | objectstack-ai#9463 | `view.test.ts:342` | "viewMode — the granularities the gantt renderer honours, measured" | Declare `viewMode` with exactly the granularities objectui's `GanttView` honours, measured, not invented (the spec half of objectui#5074's ruling). | | objectstack-ai#17053 | `view.test.ts:441` | "the legacy string `sort` clause is retired — one spelling, the array" | objectui's ruling (director batch objectstack-ai#77, option B): one spelling, the array; the spec stops producing the string. | | objectstack-ai#13704 | `view.test.ts:873` | "wizard tightening — sections are the steps, the inert step keys are refused, no key is added" | The ruled shape of objectstack-ai#13622 (2026-08-31): sections are the steps, the wizard-inert step keys are refused at parse, zero new keys. | | `objectui#6237` | `view.test.ts:949` | "… stay accepted on tabbed/simple (the ruled split confines it to wizard steps)" | Maintainer ruling 2026-08-30 (director batch objectstack-ai#3): `FormSectionConfig` is split, so tabbed sections take a predicate and wizard steps carry none. | | `objectui#2231` | `view.test.ts:2876` | "ListColumnSchema summary object form and prefix — spec-owned, no longer an objectui-local extension" | The derive-by-reference unification: `677b591` moved `prefix` and the `{ type, field }` `summary` form into the spec, closing objectui's local `.extend()`. | | objectstack-ai#3896 (see above) | `view.test.ts:3144` | "FormViewSchema — retired defaultSort (audit close-out: nothing read it)" | The landed tombstone: `form.defaultSort` was removed because nothing read it. | | `objectui#5435` | `view.test.ts:3386` | "… defaults asymmetry, copied from what the renderer reads" | Ruling A (2026-08-22): the spec adopts `group` / `hideFields` / `rowColor`, with the defaults copied from `ListView`'s reads. | | objectstack-ai#3896 (see above) | `view.test.ts:3826` | "ListViewSchema — retired responsive/performance (audit close-out: no renderer read them)" | The landed tombstones: no renderer or runtime read either key. | | objectstack-ai#7176 | `view.test.ts:3847` | "ListViewSchema — retired striped/bordered/virtualScroll (every reader only passed them through)" | Maintainer ruling 2026-08-10: retire under ADR-0049, since every measured reader copied the keys forward and none applied them. | | objectstack-ai#5832 | `view.test.ts:4099` (expect message) | "`HttpMethodType` was renamed to `HttpMethodSubset`" | Maintainer ruling 2026-08-06: rename the 5-value subset; the 7-value `HttpMethod` keeps its name and its wire contract. | | objectstack-ai#16577, objectstack-ai#13817 | `view.test.ts:4708` | "… the `type: 'calendar'` axis is NOT gated by the `allowedVisualizations` check (ruled: a completeness warning)" | Ruling B (director seat, 2026-09-11): the objectstack-ai#13817 guard keeps gating `allowedVisualizations` only; the `type: 'calendar'` route is carried at warning by `checkViewCompleteness`. | | objectstack-ai#19228 | `view.test.ts:4793` | "view row bound — `pagination.pageSize` is the one bound; no per-kind `limit` on the view configs" | Maintainer ruling D (2026-09-23): one row bound per view, `pagination.pageSize`; the per-kind `limit` was removed before it shipped. | | objectstack-ai#5055 | `widget-i18n-retirement.test.ts:70` | "ui/ widget + i18n family retirement — doorless vocabularies removed, not tightened" | Maintainer ruling A (2026-08-06): ADR-0049 enforce-or-remove retires the unreachable widget and locale vocabularies; closing them would only dress a dead slot as a checked one. | | `objectui#3289` | `widget-i18n-retirement.test.ts:196` | "the surviving `error` slot is exactly the one objectui renamed its own slot onto, with no alias" | objectui followed the spec: its widget `errorMessage` slot became the spec's `error`, with no alias, and the form renderer produces it. | **Subject back in words** (20 literals): "(binding pair, objectstack-ai#7741)" becomes "(the object + viewKind binding pair)"; "union error behaviour (objectstack-ai#5014)" becomes "union error behaviour (where a branch prescription gets buried)"; the five `objectstack-ai#7496` prefixes become "the ruled redirect `url` shape —" (twice) and "ruled bullet 1 / 2 / 3 —", the file's own name for the ruling's three bullets; "the pre-objectstack-ai#7510 ranking" becomes "the pre-fix ranking"; "the objectstack-ai#4001 wrap prescription" becomes "the `defineView` wrap prescription"; the acceptance-face describe at `view-union-branch-focus.test.ts:261` (above); "the objectstack-ai#6926 fold" becomes "the producer-side `groups` fold"; "(objectstack-ai#7025 membership)" becomes "and the union corpus pins it accepted"; "(objectstack-ai#8321/objectstack-ai#12174)" becomes "(a negative or fractional scale)", what that test probes; "the exact declaration objectstack-ai#9340 exists to make legal" and "the gap objectstack-ai#9340 closes" name "this block"; "(acceptance criterion, objectstack#11195)" becomes "(the acceptance criterion for adopting the three keys)"; "(objectstack-ai#7176 rides …)" becomes "(the retirement rides …)"; "the axis objectstack-ai#13817 does not gate" becomes "the axis the `allowedVisualizations` check does not gate"; "zero holders after objectstack-ai#5055" becomes "after the widget + i18n retirement"; "objectstack-ai#5055 — the one surviving shape" becomes "the widget retirement — the one surviving shape". **Dropped where already stated** (55 literals, 57 ids). A number goes only where the title already says its decision. Examples: the four `[objectstack-ai#19920]` prefixes ("… typed by its arm, not unknown", "… a parsed view body, not unknown") and `[objectstack-ai#19871]`; the six `[objectstack-ai#20051]` describes on the `options` bag and the one in `view-union-retirement-prescription.test.ts`; the eight `objectstack-ai#20186` describes ("a column-less list PATCH is judged by the list member", "each member judges ONE viewKind", …); the three `[objectstack-ai#6391]` describes, three `[objectstack-ai#7510]` titles and the `[objectstack-ai#21180]` table label ("the retired `publicPicker` key itself"); "(objectstack-ai#3095)", "(objectstack-ai#5074)" after "`.strip()` round-tripping is untouched", "(objectstack-ai#9933)" after "runtime-only overlay key", and the `view.test.ts` tails `(objectstack-ai#15469)`, `(objectstack-ai#6926)`, `(objectstack-ai#12174)`, `(objectstack-ai#19088)`, `(objectstack-ai#7084)`, `(objectstack-ai#9340 — …)`, `(objectstack-ai#17499)`, `(objectstack-ai#18791)`, `(framework#1894 / objectstack-ai#2998)`, `(objectstack-ai#5073 — …)` x2, `(objectstack-ai#8010)`, `[objectstack-ai#4688]`, `[objectstack-ai#4691]`, `(objectstack-ai#6416 / objectstack-ai#6619)`, `(objectstack-ai#17063)`, `(objectstack-ai#16885)`, `(objectstack-ai#13817)` and `(objectstack-ai#16577)`. The batch label `批 18` stays, in stage 20's "批 19, unknown keys refused" form on the file's first describe and bare on the other four. `W2` stays: the file's own header defines W1 and W2. The commit `ce70876e` stays in "(measured on origin/main ce70876)": a commit, not a tracker id. **No file is renamed.** ## Readers - **Test-name filters:** none. No tracked script, workflow or package config passes `-t` / `--testNamePattern` (the 3 hits are `docker build -t`, `type -t` and `lsof -t`). - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`, and none of the 16 files calls a snapshot matcher. - **Projects:** none of the 16 files is in the `repo` project (`packages/spec/vitest.repo-tests.json`); all run in `local`. - **By substring:** every old literal, its id-bearing fragment and a window around each id (299 needles) was searched with `git grep` at the base, across the tracked tree outside its own file. No gate, doc, filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test reads one. The 16 hits are windows that share wording with code comments and one CHANGELOG line: 15 comments in the migration registry, its semantic entries and `view-list-tabs-retirement.test.ts` read "(ruling B on objectstack-ai#17152)", and `packages/spec/CHANGELOG.md:30342` reads "runtime-only overlay key (objectstack-ai#9933)". - **Cross-references by id:** two places name the `columnState` section of `view-metadata-schema.test.ts` as "§objectstack-ai#9933": a code comment at `packages/spec/scripts/strictness-ledger.test.ts:380` and the `view.zod.ts` row of `docs/audits/2026-07-unknown-key-strictness-ledger.md`. Neither matches a string; both point a reader at the section, which still carries "columnState — runtime-only overlay key" in its title and its banner comment. A code comment and an audit record are not this card's share, so neither is edited. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares three lines: `view-union-branch-focus.test.ts:139`, `view.test.ts:4099` and `widget-i18n-retirement.test.ts:135`. - **Result:** 16 of 16 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 100 changed string leaves in 100 literals: 97 titles and 3 declared. The diff's `+` and `-` lines are exactly the 100 planned lines as multisets, and every file keeps its line count. - **Controls (14 of 14 as predicted on the first run, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row given an id VIOLATION; an undeclared expect message changed VIOLATION; a title re-split into a `+` chain DIFF; a declared expect message reverted to base SAME; a declared expect message given a new id VIOLATION; the declared table label given a new id VIOLATION; a template-literal title given a new id VIOLATION. - **Templates and tables:** one `.each` title changes, `view-overlay-viewkind-arm.test.ts:102`, a `%s` template whose placeholder and rows are untouched. The table label at `view-union-branch-focus.test.ts:139` feeds two `for … of` template titles, which print it whole. The template-literal title at `:173` changes only its text before `${label}`. **Test counts:** the 16 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 862 tests in 16 files, all passed, with the same count and status sequence per file in 16 of 16. 574 full test names change, and each changed name equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 16 touched files are in it, and no `*.test.ts` at all. The controls `src/ui/view.zod.ts`, `src/ui/widget.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old one each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `75022207b3`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (`VERDICT command-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 619 files, 18471 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 16 group files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date, against the `dist/` the build above wrote. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stage 22, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 16 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 16 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 200 changed lines (+100 / -100). - A control-byte scan over the 16 changed files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was four commits past the base (`1f0469655f`: objectstack-ai#21939, objectstack-ai#21937, objectstack-ai#21943, objectstack-ai#21928). They touch 32 files, none of the 16; two are under `packages/spec` (a step-18 semantic migration entry and the migration registry, neither a test file). So `main` was not merged. The census on that tree still reads 571 / 604 in test files and 0 elsewhere. `git merge-tree` onto `1f0469655f` is clean, and none of the 8 open PRs touches any of the 16 files. ## Acceptance notes - **The `{{record.FIELD}}` title.** `view-submit-redirect-url.test.ts:187` keeps its literal placeholder after "ruled bullet 2 —"; this body spells it with `FIELD` because the platform strips angle-bracket fragments from PR text. - **Same-id test titles in this card's later stages** go with those stages: 5 lines in `packages/spec/src`, `api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"), `stack.test.ts:1510` ("(objectstack-ai#17063)"), `system/stack-server.test.ts:93` and `system/translation.test.ts:672` / `:767` ("(objectstack-ai#4001)"). - **Same-id test titles in other packages** stay: 46 lines in 11 packages (`metadata-protocol` 11, `objectql` 8, `spec/scripts` 8, `lint` 6, `rest` 4, `plugin-auth` 3, `cli` 2, and one each in `plugin-security`, `plugin-sharing`, `qa/dogfood` and `service-automation`), each package's share under the objectstack-ai#20513 lane children. The three `plugin-auth` titles cite objectstack's objectstack-ai#5233, a different record from `objectui#5233`. - **Code comments with live ids** remain in these files and their sources, among them the "§objectstack-ai#9933" cross-references above, the `[objectstack-ai#7741]` / `[objectstack-ai#21180]` corpus notes in `view-union-branch-focus.test.ts` and `view-union-diagnostics.test.ts`, and the `objectstack-ai#5055` banners in `widget-i18n-retirement.test.ts`. Code comments are not this card's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
…rganization slug guard fail closed when their own read faults (objectstack-ai#21954) Fixes objectstack-ai#21941 Clause-②: no (narrowing) Two access guards used to let a request through when their own read faulted. Each now tells three answers apart: the read answered (unchanged), the object is not registered in this composition (the guard does not apply, decided from the registry), and a registered read that cannot answer (refused with `503 SERVICE_UNAVAILABLE`). This follows the triage direction in the card's triage comment: fail closed the platform's own way, not as allowed and not as the guard's own `403`. ## What changed ### `@objectstack/runtime` — `HttpDispatcher.enforceProjectMembership` (the environment-membership gate) - **The read throws** ⇒ the gate throws `AuthzStoreUnavailableError('sys_environment_member', cause)` out of `dispatch()`. This is the same loud outage the identity step and the `/keys` and activation domain gates already raise for an authorization input they could not read. The transport answers `503` with a declared `SERVICE_UNAVAILABLE` envelope. The old catch logged at debug level and returned `null` (admit). - **No ObjectQL engine resolves on the request's kernel** ⇒ refused the same way. It used to return `null` (admit). - **The request engine's registry does not register `sys_environment_member`** ⇒ the gate does not apply and nothing is read. The question is `ql.registry.getObject(...)`, the same lookup the engine's verbs make before refusing an unregistered name. An engine whose registry cannot be asked is read as before, and a fault on that read refuses. - Healthy reads keep their answers byte for byte: a member is admitted (and cached), and a non-member gets `403 PROJECT_MEMBERSHIP_REQUIRED`. ### `@objectstack/plugin-auth` — `organizationHooks.beforeUpdateOrganization` (the organization slug guard) - **The `sys_organization` read or the `sys_environment` read throws** ⇒ better-auth `APIError('SERVICE_UNAVAILABLE')` (`503`), via the new module helper `slugGuardReadFaultApiError`. The driver's error rides `cause`. Both catches used to `return`, which ended the hook without refusing, so the slug changed. - **The engine does not register `sys_environment`** (asked through `getSchema`) ⇒ the guard does not apply, and it is checked before either read. Nothing is read. - **No data engine** ⇒ the guard does not apply (unchanged `return`, now stated in code). - Healthy reads are unchanged: a slug change while an active environment references the organization is still `403 FORBIDDEN`, and anything else is allowed. ## The composition questions the triage asked to measure All readings are on `objectstack` at this branch's base, `d16b9fbf`. - **Can a composition that serves environment-scoped doors lack ObjectQL?** No, for any composition built from this repository. Only a host `KernelResolver` writes `context.environmentId`, and this repository registers none (`git grep` over `packages/**` finds `kernel-resolver` read in three places and registered in none). The gate reaches its read only for a caller the `auth` service signed in. This repository's `auth` provider, `AuthPlugin`, declares `dependencies = ['com.objectstack.engine.objectql']` (`auth-plugin.ts:291`). So the no-engine branch refuses like any other fault. - **Can a composition that mounts the organization-update door lack a data engine?** Only a standalone `AuthManager` can. `AuthPlugin` reads `ctx.getService('data')`, which throws when the service is unregistered, and the plugin hard-depends on ObjectQL. A standalone `AuthManager` runs on better-auth's in-memory store, and no engine there registers `sys_environment`. So the guard does not apply there, and the code says so. - **Is `sys_environment` registered in the open-source composition?** No. No package in this repository defines it: `git grep` finds only lookup-field references and the spec constant `CLOUD_PROVIDED_OBJECT_NAMES`, and `platform-objects/src/index.ts` says the `sys_environment*` objects are cloud-only. Pinned against the real ObjectQL registry: an engine that holds exactly `authIdentityObjects` answers `getSchema('sys_environment') === undefined`, and the guard then reads nothing. The fix decides this from registration, not from catching the throw. - **`sys_environment_member`** is also in `CLOUD_PROVIDED_OBJECT_NAMES`. The membership gate therefore asks the same registry question. ### Not measured: the cloud composition `NOT MEASURED: the cloud composition's membership gate, reason: this session was refused attaching objectstack-ai/cloud.` Two things there decide how this lands, and only the cloud tree can answer them: 1. Does the per-environment engine that `context.kernel` resolves register `sys_environment_member`? `packages/client/CHANGELOG.md` (11.0.0) records cloud#533 as retiring that object. If it is not registered there, this PR changes nothing in that composition: the gate used to admit through the caught throw and now admits by declaration. 2. If the engine does register it, does the read succeed? If that read faults on every request, every signed-in, non-platform-organization request on an environment-scoped door answers `503` after this lands. That is the triage's direction ("a real fault on a registered read refuses"), but it would be a visible change in that deployment. The cloud seat should confirm it before release. ## The HTTP door for the slug guard This was measured with a throwaway test that drove the real better-auth organization-update endpoint through `AuthManager.handleRequest`, over a memory engine double. The test was not committed. | engine | answer | slug after | `sys_environment` reads | |---|---|---|---| | does not register `sys_environment` | `200` | changed | 0 | | registers it, read faults | `503`, body `{ message }` | unchanged | 1 | | registers it, one active environment | `403`, body `{ message }` (control) | unchanged | 1 | | registers it, no environment | `200` | changed | 1 | On the `503` leg, `handleRequest` also logs one server-side line (`better-auth returned error: 503 …`). The `503` body follows better-auth's native shape, `{ message }`, the same shape the guard's own `403` refusal uses. No `code` field is added. ## Tests The pins PR objectstack-ai#21939 added now assert the refusal. Each superseded assertion is quoted in place. - `packages/runtime/src/http-dispatcher.membership-system-context.test.ts`, 13 tests: - a read that throws (plain, on a registered engine, and an engine-side `PERMISSION_DENIED`) is refused, asserted on `code`, `status` and `object`; - no engine is refused; - an unregistered object reads nothing; - a healthy registered read still admits a member and refuses a non-member; - on the wire, through `createDispatcherPlugin` on a real `ObjectKernel`: member `501` (admitted, no automation service), non-member `403 PROJECT_MEMBERSHIP_REQUIRED`, read fault `503 SERVICE_UNAVAILABLE` (the envelope parses against `ApiErrorSchema`). - `packages/plugins/plugin-auth/src/auth-manager.org-slug-guard-system-context.test.ts`, 10 tests: - an organization read fault refuses `503` and the environment read is never made; - an environment read fault refuses `503`, both on a registering engine and on one whose registry cannot be asked; - healthy registered reads still refuse and allow; - an unregistered `sys_environment` reads nothing; - with the real ObjectQL registry, `authIdentityObjects` alone reads nothing, and a real engine fault (no driver) refuses `503`. - Fixture triage, three runtime files. They did not pin the defect: - `http-dispatcher.membership-skip-boundary.test.ts` and `packages-unscoped-environment-binding.test.ts` answered the membership read from a registry that registered nothing, which the real engine refuses with `OBJECT_NOT_FOUND`. They now register `sys_environment_member`. - `domains/meta-verb-fallthrough.test.ts` composes no ObjectQL engine at all, and the gate now refuses that composition. The gate is not that file's subject, so the file sets `enforceProjectMembership: false`, as the dispatcher option documents for tests. - Fixtures with `environmentId: 'platform'` and an engine whose registry does not register the member object (for example `meta-state-plural-tolerance`) used to pass the gate through a swallowed `TypeError`. They now pass by the registry's answer. The outcome is the same. ### Ablation Each negative pin was ablated: the fail-open answer was put back, the pin turned red, and the file was restored. The mutation went through `scripts/ablation-replace.mjs`: the anchor must hit, and the blob change and restore are verified on disk, with a script `trap` plus a HEAD-blob hash proof. The subjects are imported relatively (`./http-dispatcher.js`, `./auth-manager`), so no `dist/` leg applies. Ablation was run at head `6966166a0`, with the same red counts as an earlier run at `b275f81b`. | leg | mutation | red | |---|---|---| | A1 | membership read catch → `return null` | 4/13: the three read-fault pins, and the wire pin `expected 501 to be 503` (a non-member admitted to the domain) | | A2 | no engine → `return null` | 1/13 | | B1 | organization read catch → `return` | 2/10 (`the slug change was let through …`) | | B2 | environment read catch → `return` | 2/10 | The first A2 attempt was a no-op. Its replacement re-contained the anchor, the tool refused it (anchor 1 → 1), and no test ran. A2 was redone with a different replacement. ### Results at head `6966166a0` - `pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2`: 330 files, 4662 passed, 19 skipped, 0 failed. - `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2`: 126 files, 2612 passed, 10 skipped, 0 failed. This ran at `b275f81b`. Since then `auth-manager.ts` is byte-identical, and the one changed test file was re-run at the head: 10/10. - `pnpm --filter @objectstack/runtime typecheck` and `pnpm --filter @objectstack/plugin-auth typecheck`: both exit 0, including `check:test-typecheck`. - `node scripts/pm/dispatch-gates.mjs --commands` derived 75 gate families from this diff, all run at this head, all exit 0. Reconciled with `--ran`: 75 derived, 75 run, 0 NOT-MEASURED, every exit code recorded. These include `check:dispatcher-error-vocabulary`, `check:auth-mount-ledger`, `check-system-context-census`, `check-tenant-audit-census`, `check-platform-object-tenancy-census`, `check:doc-authoring`, `check:issue-citations`, `check:nul-bytes`, `check:engine-double-contract`, `check:slot-lookup`, `check:dual-build-cjs-loads` (106 require entry points across 66 packages load) and `check-adr-0087-registration`. - `check-changeset-no-major.mjs --base origin/main --event` with this body's `Clause-②` line: exit 0. The level axis reads `no (narrowing)`, and no moved package is graded `patch`. - Lint, narrowed to the 7 changed `.ts` files with `eslint --no-inline-config --format json`: 0 errors and 0 warnings. That is 7 files linted and none ignored. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is left to CI. - One ledger row was added: `scripts/engine-double-contract.pinned.json` now records the new pinned `findOne` double, written by `check-engine-double-contract --write`. ## Acceptance notes - **Two neighbouring fail-opens in `enforceProjectMembership` are out of this card's scope and untouched.** The session-read catch ("Auth resolution failed — do not block the request on RBAC") and the `if (!userId) return null` fall-through both remain. Fixing either in place is not mechanical: the catch also covers a composition with no auth wired, which needs the registry's classified lookup. It is also unmeasured whether either is reachable through a public door, because the identity step reads the same session first, so this is read-only inference. Noted here, not filed. - **`environmentId: 'platform'`** (the reserved virtual id `rest-server.ts` documents) is skipped by `resolveRequestScope`'s helpers but not by this gate. If a host resolver ever writes it, the gate reads `sys_environment_member` for an environment id that has no rows. That behaviour is unchanged here. Noted, not filed. - **The wire `503` message is withheld** by the transport's 5xx sanitizer (`Internal server error`). The failed read is named only server-side, on the error's `object`. This is the same as the identity step's tenancy outage today. - **Changeset grade.** The dispatch asked for a `patch` changeset. Under `Clause-②: no (narrowing)`, `check-changeset-no-major.mjs` enforces `minor` for a package the diff moves. This was measured with a `patch` grade in a throwaway worktree: exit 1 (`enforce`). So the changeset is `minor` for both packages, carries the **BREAKING** banner, and records the ADR-0087 disposition `not-required (no-migration-prescription)`. --- _Generated by [Claude Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21912
Clause-②: no
This PR is a slice of #21908, the closure of the security middleware's principal-less hand-off (ADR-0096). It covers the identity and runtime producers. #21908 stays open for the deny, which lands last.
What moved
Each producer below reached the data engine with no principal and no
isSystem. That is the hand-off, and it is not an authorization. Each one now takes the explicit system opt-in that already exists. ⛔ No new elevation API, no change to what any door authorizes, no accept-set change.plugin-authauth-plugin.ts, the platform-admin OAuth client toggle route (/admin/oauth2/toggle-disabled)findOne+updatesys_oauth_applicationwithSystemContext, the wrapper better-auth's adapter already writes these rows throughplugin-authscim-connection-service.tsverifyScimBearerTokenfindOnesys_scim_connection_credentialisSystem: truein the read's trailing optionsplugin-authauth-manager.tsorganizationHooks.beforeUpdateOrganization(the slug guard)findOnesys_organization,findsys_environmentwithSystemContextruntimehttp-dispatcher.tsenforceProjectMembershipfindsys_environment_memberisSystem: trueas the read's query contextRow 20 moves nothing. I read every site, and each one already runs with the opt-in:
adopt-membership.tsadoptExistingMembership: its only caller hands it the adapter'swithSystemContextengine.membership-ended-session.tsendSessionClaimsForEndedMembership: all four calls pass{ context: SYSTEM_CTX }(isSystem: true) as the trailing options, and the engine honours that argument on reads and writes.auth-manager.tsinsert helper (settleSelfRegistrationGrant, withfindPermissionSetRows): it reads and writes throughwithSystemReadContext, the deprecated alias ofwithSystemContext.The
auth-manager.tsedit (row 19) was made after #21872 landed, on a merge oforigin/mainthat contains it.Measured: no gate fires on any moved call today
An
isSystemcontext short-circuits the gates the hand-off still runs beforenext(): package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. A move is neutral only if none of them fires on the producer's calls.sys_permission_set,sys_position,sys_capabilityandsys_position_permission_set. Engine-owned needs auserId. Delegated-administration guards writes to the RBAC link tables,sys_permission_setandsys_member. Rows 18, 19 and 21 are reads. Row 17 writessys_oauth_application, which none of the gates names.security-plugin.ts. It recorded each principal-less, non-system context that reached the hand-off, with its stack, and each gate refusal of such a context. Over every run below it recorded 0 gate refusals. Every call of the card's functions reached the hand-off, so no gate had stopped it.Per function, before → after (records at the hand-off):
findOne3,update2)verifyScimBearerToken(row 18)beforeUpdateOrganization(row 19)sys_organizationfindOne)enforceProjectMembership(row 21)Before = the base tree with the probe. After = the change with the probe: the dev boot and the harness on the final tree (
9878b925), and the dogfood subset on the pre-merge commit4239dd47, whose row 17 code is the same. The boot's background ticks (the outbox claims) make the totals differ by a few records between runs. The per-function counts are the reading.6003676228) for these rows exactly.pnpm dev -- --freshon showcase with SCIM enabled, driven as the seeded admin. It registers an OAuth client, toggles it twice, toggles a missing id, sends a SCIM request with an unknown bearer, and changes the default organization's slug. The answers were identical before and after: register 201, toggles 200 / 200 / 404, SCIM 401, slug update 200.SecurityPluginand calledenforceProjectMembershipfor a member and a non-member. No open-source composition reaches row 21: noKernelResolversetsenvironmentId, andsys_environment_memberis a cloud control-plane object. The answers werenulland 403 both times.security-plugin.tsblob5b4ab280equals HEAD),plugin-securitywas rebuilt, andablation-dist-preflight --absentconfirms the marker is gone fromdist/. The positive control: 4 hits indist/while the probe was live.One difference that is not a gate (row 17). Under the hand-off, the engine's static read-only strip ran on the toggle's
updateand dropped theupdated_atthe route supplies, with a WARN. UnderisSystemthe strip does not run. I compared the stored rows: on the SQL driver, both paths storedisabledand anupdated_atequal to the driver's own stamp. The only change is that the WARN line no longer appears on each toggle.Pins (one per package) and ablations
plugin-auth/src/principal-less-producers-system-context.test.ts: a real engine with a context-recording middleware. The toggle route'sfindOneandupdateand the SCIM probe'sfindOneareisSystem. The route still answers 200 and flips the stored flag, and still answers 404RESOURCE_NOT_FOUND. The verifier still resolves a known bearer to its connection, and still answersnullfor an unknown one.plugin-auth/src/auth-manager.org-slug-guard-system-context.test.ts: both slug-guard reads areisSystem, and the guard still refuses withFORBIDDEN/ 403 while an active environment exists. On an engine that refuses a principal-less, non-system context, the guard still refuses. The pre-existing catches are pinned as they stand: a read that throws ends the hook without refusing.runtime/src/http-dispatcher.membership-system-context.test.ts: the membership read isisSystem, a member passes, and a non-member gets 403PROJECT_MEMBERSHIP_REQUIRED. On an engine that refuses a principal-less, non-system context, the non-member is still refused. The pre-existing fail-open catch is pinned as it stands: a read that throws lets the request through.scripts/ablation-replace.mjs: the anchor hit once, the mutation was verified on disk, and the restore was proven (blob equals HEAD,git diff HEADempty). Each pin imports its subject fromsrc, so no build sat between the mutation and the run.withSystemContextdropped: 2 red.9878b925): 3 red, including the refusing-engine non-member case.withSystemContextdropped: 2 red, including the refusing-engine case.Tests and gates (at
9878b925)9878b925: plugin-auth 2 files, 9/9 passed. runtime 1 file, 5/5 passed.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2on9878b925: 330 files, 4654 passed, 19 skipped.pnpm --filter @objectstack/runtime run typecheck: exit 0.60c5f22c: the suite (126 files, 2607 passed, 10 skipped) andrun typecheck(exit 0). The only commit since,9878b925, touchesruntimeand the changeset, and plugin-auth imports neither.toHaveBeenCalledWithinhttp-dispatcher.test.tsandhttp-dispatcher.membership-skip-boundary.test.ts. They turned red. The context now rides inside the query instead. The opt-in is the same and so is the engine's reading (ObjectQL merges the two), and both suites pass unedited.eslint --no-inline-configover the 7 changed.tsfiles: 7 files, 0 errors, 0 warnings. These 7 are the whole population whose lint verdict this diff can move.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules), so no untouched file's verdict can change. The repo-widepnpm lintis CI's.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon9878b925derived 98 commands. All 98 ran and exited 0. The--ranreconciliation over the exit-coded record reads: "98 derived famil(ies) accounted for — 98 run, 0 NOT-MEASURED (a DERIVED zero — all 98 recorded an exit code and none of them is 3)". The same 98 also ran green on60c5f22c.origin/main(9dce6353). Those commits touchcontent/docs/permissions/sso.mdxand aresttest, none of this diff's files. fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage #21902 is merged intomainand contained in this branch.Acceptance notes
isSystem. A read that throws for any other reason still skips the slug guard (row 19) or opens the membership gate (row 21). Both behaviours are pinned as they stand, so the seat can sequence them before the deny.sys_environmentis not registered there, so the environment read throws before it reaches the engine middleware. The catch then ends the hook, and the slug guard never refuses in an open-source deployment. It acts only where the object exists. Measured on the dev boot: the slug change answered 200 and recorded no environment read at the hand-off.isSystemread also bypasses any host read hook keyed on the caller, such as a control-plane org-scope hook. I measured the six named gates only, and only in-repo.mintScimConnectionCredentialinserts without the opt-in. It has no runtime caller (tests only) and is not exported from the package entry, so nothing produces through it today. Noted, not changed.content/docs/permissions/system-context.mdx) is current.--fixmoved its held declaration count from 25 to 26, for the new trailing-options type on the SCIM probe. It asked for no anchors.Generated by Claude Code