Skip to content

Commit ba5e957

Browse files
Jack Qclaude
andauthored
feat(lint): permission/book cross the runtime publish gate; object measured dirty stays behind; security-role-word splits to stay behind whole (#8310) (#8546)
* feat(lint): validateSecurityPosture gates permission/book at the runtime publish door; security-role-word splits out to stay behind whole (#8310) The #7891 flip, measured per #4001: permission and book cross (zero refusals on the full metadata-protocol suite and an 88-write corpus replay); object stays behind — re-measured dirty one package over (objectql 83 tests all security-owd-unset; rest 12 tests incl. #7674's 403-door contract pins) and escalated on the card. security-role-word becomes its own CLI-only registry entry (validateSecurityRoleWord) so one rule id sits on one side of the wall (#7220) instead of enforcing for a subset of its six collections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL * test(lint): pin the measured partial flip — permission/book at the real gate, object's residue via the snapshot builder (#8310) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 79e5931 commit ba5e957

6 files changed

Lines changed: 515 additions & 274 deletions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `validateSecurityPosture` now gates `permission` / `book` writes at the runtime publish door; `security-role-word` splits into its own CLI-only rule (#8310)
6+
7+
The measured half of the #7891 strictness rollout (#4001 pattern): the
8+
`validateSecurityPosture` registry entry declares `runtimeTypes: ['seed',
9+
'permission', 'book']`, so the security-posture rule families run at the
10+
runtime metadata publish gate (Studio, REST `/meta`, MCP/AI authors) for
11+
permission-set and book writes. A class of runtime writes that used to succeed
12+
can now be refused with a 422 — e.g. a permission set granting a `'*'`
13+
wildcard with View/Modify All Data (`security-wildcard-vama`) or a
14+
high-privilege default set (`security-anchor-high-privilege`). Warning/info
15+
findings (the three cross-collection rules' common verdicts) surface as
16+
non-blocking advisories on the save response.
17+
18+
Measured zero breakage for the shipped set: the full
19+
`@objectstack/metadata-protocol`, `@objectstack/objectql` and
20+
`@objectstack/rest` suites plus a replay of every shipped-corpus
21+
permission/book/seed write (showcase + CRM + todo + blank) produce zero
22+
refusals.
23+
24+
`object` is deliberately NOT declared: re-measured on the #8308-repaired tree
25+
it still refuses 95 platform-suite writes in `@objectstack/objectql` and
26+
`@objectstack/rest` (all `security-owd-unset` / `security-external-wider`),
27+
including contract pins of the ADR-0094 403 `owd_external_wider` door that
28+
this 422 gate would preempt — escalated on #8310 rather than forced green.
29+
30+
`security-role-word` does not cross either: it judges six collections
31+
including `positions`/`apps`, which the per-write snapshot does not carry — so
32+
it is split into its own CLI-only registry entry (`validateSecurityRoleWord`,
33+
same rule id, same findings on every CLI command) rather than enforced for a
34+
subset of its collections (#7220: one rule id sits on one side of the wall).
35+
`validateSecurityRoleWord` is a new named export of `@objectstack/lint`;
36+
`validateSecurityPosture` no longer emits `security-role-word` findings —
37+
callers consuming both should run both (the registry does).

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ import { validateSeedReplaySafety } from './validate-seed-replay-safety.js';
122122
import { validateSeedStateMachine } from './validate-seed-state-machine.js';
123123
import { validateVisibilityPredicates } from './validate-visibility-predicates.js';
124124
import { validatePredicatePathRefs } from './validate-predicate-path-refs.js';
125-
import { validateSecurityPosture } from './validate-security-posture.js';
125+
import { validateSecurityPosture, validateSecurityRoleWord } from './validate-security-posture.js';
126126
import { validateOrgAxisRedLines } from './validate-org-axis-red-lines.js';
127127
import { validateSharingRuleEnforceability } from './validate-sharing-rule-enforceability.js';
128128
import { validateRlsPredicateEnforceability } from './validate-rls-predicate-enforceability.js';
@@ -1126,75 +1126,110 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11261126
// sees one refusal, never two. The stated cost of moving was imaginary; the
11271127
// reason it has not moved is the measured one below.
11281128
//
1129-
// Why the move is not taken HERE, measured rather than assumed (#7576 stage 1):
1129+
// The move IS taken now — the #7891 programme's three slices, in order:
11301130
//
1131-
// - The four shipped stacks (showcase, CRM, todo, the `blank` template — 30
1132-
// objects, 10 permission sets, 1 book, 12 positions, 3 apps, 24 seeds) are
1133-
// CLEAN of `error` findings at both surfaces. No shipped app trips.
1134-
// - The PLATFORM's own runtime write path does. Declaring `object` here makes
1135-
// `security-owd-unset` refuse any object published without an OWD, and that
1136-
// is the shape the runtime create door actually emits: it turns 26 writes
1137-
// into 422s across 8 files of `@objectstack/metadata-protocol`'s own suite,
1138-
// and `METADATA_CREATE_SEEDS.object` — the authoritative minimal create body
1139-
// — carries no `sharingModel` either. That is a strictness rollout
1140-
// (#4001 pattern), not a registry-honesty fix, and its repair sites are in
1141-
// packages this card may not edit.
1142-
// - `permission` and `book` failed for a different, structural reason —
1143-
// REPAIRED under #8309. The gate used to carry `objects` as resolution
1144-
// context and nothing else (`RuntimeStackContext`), so the three
1145-
// cross-collection rules judged a snapshot missing the collection they
1146-
// compare against. Measured: one simulated runtime write per shipped
1147-
// permission set produced 38 `security-master-detail-ungranted` warnings
1148-
// where the same rule over the whole stack produces 4 — with one set in
1149-
// the snapshot, every detail object the tenant's OTHER sets grant read as
1150-
// ungranted. `security-private-no-readscope` and
1151-
// `security-book-audience-unknown-set` failed identically. The snapshot
1152-
// now carries `permissions` and `books` in both differential passes and
1153-
// `TYPE_TO_STACK_KEY` maps both types, so the per-write verdict agrees
1154-
// with the whole-stack one (pinned in
1155-
// `validate-security-posture.runtime-surface.test.ts`); what remains for
1156-
// these two types is ONLY the `runtimeTypes` declaration itself — #8310.
1131+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
1132+
// the isolation proof that the differential cancels every finding this
1133+
// function derives from the sibling collections.
1134+
// - #8309: the snapshot repair. The gate used to carry `objects` and
1135+
// nothing else, so the three cross-collection rules judged a universe
1136+
// missing the collection they compare against (measured: 38 phantom
1137+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
1138+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
1139+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
1140+
// - #8310 (this state): `runtimeTypes` gains `permission` + `book`.
1141+
// `object` measured DIRTY and stays behind — see below.
11571142
//
1158-
// The residue that WAS ready, and now crosses (#8307): the two ADR-0091 seed
1159-
// rules (`security-grant-expired-at-authoring`, `security-delegation-missing-
1160-
// reason`) read only `stack.data[]` and cross the wall together as a whole
1161-
// sub-family, with zero measured trips (re-run for this card — see
1162-
// `validate-security-posture.runtime-surface.test.ts`). `security-role-word`
1163-
// is deliberately NOT in this slice: it judges six collections, and wiring the
1164-
// two that need no snapshot would split ONE rule id across the wall — a door
1165-
// where a position named `sales_role` is refused and an object named
1166-
// `sales_role` is not, which is the #7220 failure this table already refuses
1167-
// to build.
1143+
// Why `object` is still not declared, re-measured on the #8308-repaired
1144+
// tree rather than inherited (#4001: zero breakage is demonstrated, never
1145+
// assumed). The OLD blocker is genuinely gone: with `object` declared, the
1146+
// full `@objectstack/metadata-protocol` suite passes (the 26-refusal
1147+
// measurement predates #8308's `METADATA_CREATE_SEEDS.object` repair) and a
1148+
// replay of every shipped-corpus object through the real gate refuses
1149+
// nothing. But one package over the same declaration still breaks the
1150+
// platform's own write paths — measured on this exact tree:
11681151
//
1169-
// `object` / `permission` / `book` remain UNDECLARED here (that is #8310):
1170-
// (a) declaring `object` makes `security-owd-unset` refuse every OWD-less
1171-
// runtime object publish — a strictness rollout (#4001), not a wiring fix;
1172-
// (b) `permission` / `book` needed a second collection the per-write
1173-
// snapshot did not carry, and were measured inventing findings without it
1174-
// (38 vs 4 over the shipped corpus). #8309 closed (b) — the snapshot now
1175-
// carries `permissions`/`books` and both types map to stack keys — so
1176-
// declaring them here is #8310's remaining one-line flip. This
1177-
// entry is the WHOLE `validateSecurityPosture` function (all 13 rule ids), not
1178-
// a per-rule split: `runtimeTypes: ['seed']` is safe to declare on the whole
1179-
// entry ONLY because of `runtime-gate.ts`'s baseline/candidate differential —
1180-
// a `seed` write's candidate stack carries `objects` IDENTICAL to the
1181-
// baseline (only `data` differs), so every finding this function derives from
1182-
// `stack.objects` / `stack.permissions` / `stack.positions` / `stack.apps` /
1183-
// `stack.books` is produced, byte-identical, in BOTH passes and cancels in the
1184-
// diff — only the ADR-0091 pair's `stack.data[]` reads can differ. Splitting
1185-
// this entry into a seed-only registration is not needed for correctness; it
1186-
// would only be needed if a future rule in this function read `stack.data[]`
1187-
// in a way that also depended on `stack.objects` context (none does today).
1152+
// - `@objectstack/objectql`: 83 tests across 13 files fail, every one
1153+
// `security-owd-unset` (85 refusals) — the suites publish objects with
1154+
// no authored `sharingModel` through `saveMetaItem`.
1155+
// - `@objectstack/rest`: 12 tests across 3 files — the same owd-unset
1156+
// class, PLUS two genuine CONTRACT collisions no fixture edit can
1157+
// honestly settle: `meta-object-owd-gate.test.ts` pins #7674's ADR-0094
1158+
// 403 `owd_external_wider` door, which this 422 gate now PREEMPTS for
1159+
// the same defect (`saveMetaItem` runs this table first), and it pins
1160+
// that a write with NO OWD keys at all SAVES (ADR-0094 reads absence as
1161+
// the D1 `private` default) — which `security-owd-unset` exists to
1162+
// refuse (absence must be an authored decision).
1163+
//
1164+
// So declaring `object` is not a wiring fix and not even only fixture
1165+
// repair in two packages outside this card's surface: it is a decision
1166+
// about which door answers for OWD defects (403 ADR-0094 vocabulary vs
1167+
// 422 lint vocabulary) and whether an unauthored OWD refuses at runtime.
1168+
// That decision is escalated on #8310; until it is ruled, `object` stays
1169+
// undeclared and the pins in
1170+
// `validate-security-posture.runtime-surface.test.ts` record both what
1171+
// WOULD happen (via the gate's own snapshot builder) and that it does not.
1172+
//
1173+
// `security-role-word` is NOT in this entry any more — that is what the
1174+
// `validateSecurityRoleWord` entry below records. It judges six collections
1175+
// (objects, fields, actions, permission sets, positions, apps — plus books),
1176+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
1177+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
1178+
// function that still contained it would have enforced ONE rule id for a
1179+
// strict subset of its collections: a door where a permission set named
1180+
// `role_manager` is refused and a position named `sales_role` walks through
1181+
// — the #7220 failure this table refuses to build, in either direction. The
1182+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
1183+
// entry.
1184+
//
1185+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
1186+
// registration, not a per-rule split: the baseline/candidate differential is
1187+
// what keeps a write of one declared type from leaking the other rules'
1188+
// whole-stack findings — every finding derived from a sibling collection is
1189+
// produced byte-identically in both passes and cancels in the diff. Only
1190+
// findings the written item itself adds are attributed to the write.
11881191
{
11891192
name: 'validateSecurityPosture',
11901193
tier: 'gating',
11911194
input: 'parsed',
11921195
commands: ALL,
11931196
source: 'packages/lint/src/validate-security-posture.ts',
11941197
surfaces: CLI_AND_RUNTIME,
1195-
runtimeTypes: ['seed'],
1198+
runtimeTypes: ['seed', 'permission', 'book'],
11961199
run: (stack) => validateSecurityPosture(stack),
11971200
},
1201+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
1202+
// `validateSecurityPosture` the day the rest of that block crossed the
1203+
// runtime wall — so that it could stay behind WHOLE rather than cross for
1204+
// three of the six collections it judges (#7220: one rule id must sit on ONE
1205+
// side of the wall). The split is a surface boundary, not taste: the rule's
1206+
// verdict and findings are byte-identical to before on every CLI command
1207+
// (both entries run on all three), and the runtime door does not run it for
1208+
// ANY type.
1209+
//
1210+
// The road to crossing is concrete and short, recorded here so the next
1211+
// seat prices it correctly: carry `positions`/`apps` in
1212+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
1213+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
1214+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
1215+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
1216+
// #7220 discipline satisfied.
1217+
{
1218+
name: 'validateSecurityRoleWord',
1219+
tier: 'gating',
1220+
input: 'parsed',
1221+
commands: ALL,
1222+
source: 'packages/lint/src/validate-security-posture.ts',
1223+
surfaces: CLI_ONLY,
1224+
surfaceReason:
1225+
'P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, ' +
1226+
'positions, apps — plus books), and the per-write snapshot neither carries nor maps ' +
1227+
'positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of ' +
1228+
'its six collections — the #7220 split (an object named sales_role refused while a position ' +
1229+
'named sales_role walks through). It crosses whole — positions/apps carried, mapped and ' +
1230+
'declared — or stays behind; it stays behind until that wiring exists.',
1231+
run: (stack) => validateSecurityRoleWord(stack),
1232+
},
11981233
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
11991234
// sharing rule that walks it builds a second permission hierarchy (the
12001235
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,

‎packages/lint/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export type { SeedStateMachineFinding, SeedStateMachineSeverity } from './valida
237237

238238
export {
239239
validateSecurityPosture,
240+
validateSecurityRoleWord,
240241
SECURITY_OWD_UNSET,
241242
SECURITY_OWD_ALIAS,
242243
SECURITY_EXTERNAL_WIDER,

0 commit comments

Comments
 (0)