feat(lint): ADR-0091 seed pair crosses the runtime publish gate (#8307) - #8390
Conversation
Register runtimeTypes: ['seed'] on the validateSecurityPosture registry entry (packages/lint) so security-grant-expired-at-authoring and security-delegation-missing-reason enforce at the runtime metadata write door for a seed publish, per the #7891 programme's #4001-pattern staging. Re-measured trip-free rather than inherited from #7576/#8308: none of the four shipped stacks (showcase, CRM, todo, blank) author any seed row on sys_user_position / sys_user_permission_set at all (grep across every defineSeed() call site), so the ADR-0091 loop body never executes for the current corpus -- structurally zero, not merely counted zero. The other eleven rule ids this one registry entry also carries (object / permission / book posture, security-role-word, ...) stay undeclared -- that is #8310, still blocked on a strictness rollout and RUNTIME_NEEDS_FULL_SNAPSHOT. Declaring runtimeTypes: ['seed'] on the WHOLE entry rather than splitting it is safe because runtime-gate.ts's baseline/candidate differential holds stack.objects identical across both passes for a seed write, so every other-rule finding fires identically in both passes and cancels in the diff -- proven in validate-security-posture.runtime-surface.test.ts, including a positive control (a trippy object context that WOULD leak if the isolation failed). Extends validate-security-posture.runtime-surface.test.ts with the pin recording the crossing (real runRuntimeAuthoringRules calls, not the pre-crossing mirror) and a changeset. Fixes #8307 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
… on validateSecurityPosture's reach (objectstack-ai#8547) (objectstack-ai#8866) Two test comments justified the `package-author` carve-out with a claim about where `validateSecurityPosture` is enforced, and that claim rotted twice. `protocol.runtime-authoring-gate.test.ts` still said the rule is `CLI_ONLY` in `AUTHORING_RULES`. The entry declares `surfaces: CLI_AND_RUNTIME` with `runtimeTypes: ['seed', 'permission', 'book', 'object']` — false since PR objectstack-ai#8390, and the `object` limb landed with PR objectstack-ai#8600. `meta-object-owd-gate.test.ts` had already lost the words `CLI_ONLY` but still concluded package authoring is "gated at build time instead", contradicting its own file header and the sibling comment 214 lines above, both of which say the rule now runs for `object` writes at that door. Neither site is repaired by a word swap. The carve-out is intact and was never the defect: both doors skip the `package-author` channel deliberately — `assertRuntimeAuthoringRules` returns early on it at every call site and the single `runAuthoringGate` call is guarded by the same check. The reason is re-founded on that channel rather than on the rule's reach, which moves with every objectstack-ai#7891 slice. Door order and ADR-0094's R1-stays / R2-retired outcome are pointed at where they are already pinned rather than restated a second time, and the surviving `isSystem` / `sys_*` exemption is recorded as fact with a pointer to the open decision card. Comment prose only: the diff touches `//` and ` *` lines exclusively. No behaviour, assertion, pin or test-count change. Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #8307
Programme slice of #7891 (staging per #4001 pattern, #7220/PR #7479 precedent — the strictness-rollout direction is ruled, not re-decidable in this slice).
What changed
packages/lint/src/authoring-rules.ts— thevalidateSecurityPostureregistry entry now declaressurfaces: ['cli', 'runtime-publish']withruntimeTypes: ['seed'](previouslyCLI_ONLYwith asurfaceReason). This puts the ADR-0091 seed pair —security-grant-expired-at-authoring,security-delegation-missing-reason— on the runtime metadata publish door (Studio, REST/meta, MCP/AI authors) for aseedwrite, per #7576's own staging note that this pair was the one slice ready to cross.Why declaring it on the WHOLE registry entry is safe
This entry is the WHOLE
validateSecurityPosturefunction (all 13 rule ids) — there is no per-rule-id split inauthoring-rules.ts.runtime-gate.ts's baseline/candidate differential is what makesruntimeTypes: ['seed']safe to declare here without leaking the other 11 rule ids' verdicts: aseedwrite's candidate stack carriesobjectsIDENTICAL to the baseline (onlydatadiffers), so every finding this function derives fromstack.objects/stack.permissions/stack.positions/stack.apps/stack.booksis produced byte-identically in both passes and cancels in the diff. Only the ADR-0091 pair'sstack.data[]reads can differ between the two passes. This is proven — not just asserted in a comment — by a new positive-control test: aseedwrite against a context object that WOULD tripsecurity-owd-unsetandsecurity-role-wordif the isolation failed, asserting zero leaked findings.object/permission/bookremain undeclared — that is #8310, still blocked (a strictness rollout onobject,RUNTIME_NEEDS_FULL_SNAPSHOTonpermission/book).security-role-wordis deliberately not in this slice either (#7220 constraint — it judges six collections and wiring only the snapshot-safe two would split one rule id across the wall).Re-measured, not inherited (hard constraint from the dispatch)
The dispatch required re-running the "trip-free on the shipped corpus" claim rather than trusting #7576/#8308's numbers. I grepped every
defineSeed(...)call site across all four shipped stacks (showcase, CRM, todo, theblankcreate-objectstack template):examples/app-showcase/src/data/seed/index.tsseeds Account, Contact, Project, Task, Category, BusinessUnit, Team, Product, ProjectMembership, FieldZoo, Invoice, InvoiceLine, ExpenseReport, ExpenseLine, Inquiry, Preference, Announcement.examples/app-crm/src/data/index.tsseeds Account, Contact, Opportunity, Lead, Activity.examples/app-todo/src/data/index.tsseeds Task.packages/create-objectstack/src/templates/blankhas no seed data at all.None of the four apps authors a
sys_user_positionorsys_user_permission_setseed row — grep forvalid_until,delegated_from, and either object name across all three source trees returns zero hits. Showcase's own approval demo (src/security/seed-approval-demo.ts) explicitly does this at runtime instead, precisely because "users can't be seeded (they sign up) and position assignments are runtime admin actions." So the ADR-0091 loop body (GRANT_SEED_OBJECTS.has(seedObject)) never executes for any of the four shipped apps — this is a structural zero for the current corpus, not a measured-and-hoped-stays-zero one. No STOP condition was hit.Tests
packages/lint/src/validate-security-posture.runtime-surface.test.tsextended with the pin recording the crossing:surfaces: ['cli', 'runtime-publish']/runtimeTypes: ['seed'];runRuntimeAuthoringRules({ type: 'seed', ... })(previously only the pre-crossing mirror could be asked);object/permission/book/position/appstill reach no rule (unaffected — [3 of #7891] Flip the registration:runtimeTypesgainsobject+permission/book— whole rule families cross the publish door #8310's residue).Local gates (
node scripts/pm/dispatch-gates.mjs <changed paths>)Re-derived against the actual committed diff (
git diff --name-only 2d8dba312 HEAD) — same file set as before commit, no drift:check:spec-parsed-alias— OKcheck:cross-package-test-inputs— OK (self-test + real run)check:changeset-gate-self-tests(empty-changeset / adr-0087-registration / changeset-no-major self-tests) — OKcheck:objectui-changeset,check:objectui-pin-freshself-tests — OKnode scripts/check-changeset-no-major.mjs— "This diff introduces nomajorbump"node scripts/check-adr-0087-registration.mjs— "this PR adds no declared-breaking changeset" (correct: this is additive enforcement, not a spec key removal/rename)check:query-options-erasure— ratchet holds, no new unswept sites;check:type-check-coverage— unaffected,@objectstack/linttypechecks cleancheck:nul-bytes— OK, no raw control bytescheck:api-surfaceis untouched, as expected — nopackages/specsource in this diff.Changeset
.changeset/security-posture-seed-pair-runtime-publish.md,"@objectstack/lint": minor— following the direct precedent of.changeset/visibility-predicate-family-runtime-publish.md(#7220's own family crossing the runtime-publish wall, alsominor) rather than the dispatch prompt's default "patch unless conventions say otherwise": this is a real behavior change (a class of runtime writes that used to succeed will now be refused), and the established convention for "a rule crosses ontoruntime-publish" in this repo isminor, notpatch.Scope note
File surface matches the dispatch exactly: the
validateSecurityPostureregistration inpackages/lint, the runtime-surface pin extension, and the changeset. Noobject/permission/bookwiring, nosecurity-role-word. Did not touch #8273's (error-code-ledger + service-settings) or #8057's (objectql engine + kernel schema) files.Generated by Claude Code