Repository navigation
[finding] validateSecurityPosture's CLI_ONLY surfaceReason claims coverage the ADR-0094 object authoring gate does not give it — it covers 1 of the block's 13 rules #7576
Description
Activity
Triage:
findingapplied (disjunction-3 repair — the card arrived withdomain:spec-toolingbut no pm-state, invisible to the queue view). Grade matches the filer's own framing: measured coverage-claim falsity, deliberately filed-not-fixed, with a suggested shape that is explicitly "not a decision" and needs the block-split vsruntimeTypesdesign taken deliberately against the wiring test.- Anchors re-verified on
origin/main@34c01a5: the block registration with the verbatimsurfaceReasonsits atpackages/lint/src/authoring-rules.ts:1060-1071(surfaces: CLI_ONLY, no per-rule surfaces);object-posture-gate.tsreads exactlysharingModel/externalSharingModelthrough its localOWD_WIDTH(:39,:73) — consistent with the 1-of-13 measurement. - Routing:
domain:spec-toolingwas applied by the filing seat (its dispatch instruction for Publish-time lint:sharingModel: controlled_by_parentwith nomaster_detailrelation is statically detectable and unreported #7503 told it to file the gap separately); content-correct per the domain table (lint rules围着 spec 契约转), label respected as-is — noted for the single-producer record, not reverted. - Dup check: [finding] The four
views[]visibility-predicate rules are CLI-only — a Studio/REST/MCPviewwrite bypasses all of them; if they move to runtime-publish, they must move together #7220/PR feat(lint): move the views[] visibility-predicate family to the runtime publish gate (#7220) #7479 moved six rule ids on the same axis (closed, hours earlier — distinct rules); [P3] Generalise the lint rulesurfacesaxis to an open N-surface dimension (#4463 P1 introduced a closed two-value one) #7443 is on hold and explicitly not restarted by this card; [runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 is the named P2 consolidation. No open card claims this block'ssurfaceReason. - Promotion path: promote when someone takes the surface change deliberately; the falsity of the stated reason alone doesn't page anyone today (
assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474's runtime refusal covers the sharpest instance).
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Anchors re-verified on
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote, with a care note. A
surfaceReasonclaiming ADR-0094-gate coverage for 13 rules when the gate carries 1 is a false coverage declaration over the security-posture block — 12 rules enforced nowhere at runtime while the registry says otherwise. Care note for the dispatch: moving 13 rules onto a live surface is a strictness expansion — measure the existing corpus first (showcase + templates); if any shipped app trips, that finding escalates per the #4001 strictness-rollout pattern instead of riding this card.finding→pm:queue(domain:spec-tooling).
Generated by Claude Code
Queued behind #7696 — deliberately serialized, not overlooked. This card is graded, promotable and next in line; it is not being dispatched yet for one measured reason.
⛔ Not a hold: no
pm:on-hold, no restart condition needed.pm:queuestands. It dispatches the moment #7696's PR lands.Why serialized
This lane's seat post (#6018) carries a 热文件串行队 section, and
packages/lint/src/runtime-gate.test.tsis on it:- [finding] Two publish-time rules give an author contradictory fixes for the same bare unquoted word —
visibility-bare-identifier(error) saysdata.active,predicate-rhs-path-shaped(warning) says'active'#7696 (dispatched this round, sessionsession_01VazgUHoW88FzFkGykVzJLM) rewrites the region of that file pinning thevisibility-bare-identifier/predicate-rhs-path-shapedcontradiction. - This card moves
validateSecurityPosture's block onto a live surface — 13 rules in one field — which necessarily changes what the runtime gate runs, and therefore that file's expectations.
Two agents rewriting expectations in one test file is the collision the serialization queue exists to prevent. The regions are adjacent enough that "different describe blocks" is not a safe assumption, and this card is the larger, riskier of the two — so the small one goes first. #7696's dispatch order already forbids it from reformatting that file beyond its own region.
Carried forward to the dispatch — triage's care note, which changes the shape of this card
"Moving 13 rules onto a live surface is a strictness expansion — measure the existing corpus first (showcase + templates); if any shipped app trips, that finding escalates per the #4001 strictness-rollout pattern instead of riding this card."
⚠️ That makes corpus measurement the first deliverable, before any registry edit — and it means this card has a legitimate outcome in which the surface move does not land here. Recorded now so it is not lost between rounds.Two more constraints already established and not to be re-derived:
packages/lint/src/authoring-rule-wiring.test.tspolices this exact field — the change has to be made deliberately, with thesurfaceReasonrewritten to whatever survives.- ⛔ Eight of the thirteen rules judge permission sets, books or seed data and never reach an
objectbody at all. A block registered forobjectonly would still not run them — so "flip the block toCLI_AND_RUNTIME" is not by itself a correct fix, andruntimeTypes(or a split into per-collection registrations) is part of the real shape. ⚠️ [P3] Generalise the lint rulesurfacesaxis to an open N-surface dimension (#4463 P1 introduced a closed two-value one) #7443 stays on hold regardless. It generalises thesurfacesaxis to N surfaces; its restart condition is a third value actually proposed forAUTHORING_SURFACES, and ⛔ adding aruntimeTypesvalue does not trip it.
Generated by Claude Code
- [finding] Two publish-time rules give an author contradictory fixes for the same bare unquoted word —
Claim: PM loop round 1 (takeover shift,
domain:spec-toolingseat #6018)
Session:session_01WocN37om5bw81JDoEEMA2e
Branch:claude/issue-7576-security-posture-surfaces
Worktree:objectstack-issue-7576(cloud session — fresh clone, own container)
Domain:domain:spec-tooling
File surface:packages/lint/src/authoring-rules.ts— thevalidateSecurityPostureblock registration region ONLY (surfaces/surfaceReason/ possible split into per-collection registrations);packages/lint/src/validate-security-posture.ts(docblock/table only if the registration shape changes);packages/lint/src/authoring-rule-wiring.test.ts(the field's police); plus a corpus-measurement record. ⛔ Nopackages/spec/src/**, nopackages/plugins/plugin-security/**(runtime half is another lane's; #7220 measured that the lint-side registration declaration is sufficient). Stop on breach; explain in the report.
Container & model: M (design-weight: one field moves 13 rules' surface;runtimeTypes/split shape must be designed against the wiring test),mode:cloud,model: opus
Serial constraints cleared: #7696 landed (PR #7810 merged), #7659 landed (PR #7691), #7503 landed (PR #7574), #7094 landed (PR #7805). The only in-flight PR in this lane, #7808 (#7658), touchespackages/spec/scripts/**+ generated docs only — disjoint. Hot-file rule from seat #6018 honored: no per-rule card rides beside this block-level card inauthoring-rules.ts. #7443 stayspm:on-hold; its restart condition (a thirdAUTHORING_SURFACESvalue) is not tripped byruntimeTypeswork.Pre-dispatch premise re-checks, taken at
origin/main0dcbc11(2026-08-12T01:1xZ): block stillsurfaces: CLI_ONLYwith the verbatimsurfaceReason; rule-id family re-enumerated = 13 constants (matches the card's table); the posture gate (object-posture-gate.ts, now 140 lines) still reads exactlysharingModel+externalSharingModelthroughOWD_WIDTHand neverfields;⚠️ the suite's own emit-site pin has moved 15 → 16 (pushedRuleIds()).toHaveLength(16)) since #7503's review — the dev re-enumerates at work time rather than trusting any quoted count.
Generated by Claude Code
os-dev report — #7576 · branch
claude/issue-7576-security-posture-surfaces· PR #7886 (draft) · sessionsession_017eJ3PgHdf8KtoSvUVsR4qoOutcome: the third branch of the ruling — measurement +
surfaceReasonhonesty fix. The surface move does NOT land here.Part of #7576, notFixes, so the card stays open while the remainder escalates.Stage 1 — corpus measurement (first deliverable, taken before any registry edit)
Four shipped stacks, loaded through their real
objectstack.config.tsand judged by the real rule.packages/platform-objectsships no seed metadata, so it contributes no rows.stack objects permissions books positions apps seeds (records) examples/app-showcase23 (202 fields) 8 1 9 1 18 (130) examples/app-crm6 (49 fields) 2 0 3 1 5 (28) examples/app-todo1 (20 fields) 0 0 0 1 1 (8) templates/blank1 0 0 0 0 0 Probe A = whole-stack (what the three CLI commands already run). Probe B = the per-write differential the runtime gate performs, one simulated write per item per collection.
rule id sev A B security-owd-unseterror 0 0 security-owd-aliaserror 0 0 security-external-wider-than-internalerror 0 0 security-controlled-by-parent-no-relationerror 0 0 security-wildcard-vamaerror 0 0 security-anchor-high-privilegeerror 0 0 security-role-worderror 0 0 security-fls-unqualified-keyerror 0 0 security-grant-expired-at-authoringerror 0 0 security-delegation-missing-reasonerror 0 0 security-master-detail-ungrantedwarning 4 38 (all permissionwrites)security-private-no-readscopeinfo 2 2 ( permissionwrites)security-book-audience-unknown-setwarning 0 0 Zero
errorfindings anywhere — no shipped app trips. Positive controls for the zero rows:validate-security-posture.test.ts'sREACHABILITY_CORPUSalready carries one deliberately-violating fixture per rule id (all 16 emit sites proven reachable); the PR adds the runtime-shaped controls (OWD-less object write refused, expired seed grant refused, undocumented delegation refused,controlled_by_parent-without-relation refused). The one rule with no shipped instance either way issecurity-book-audience-unknown-set— the single shipped book declaresaudience: 'public', not{ permissionSet }— so its control is the fixture.Why stage 2 did not land
1.
objectis a strictness rollout, and its repair sites are outside this card. Measured by making the change and running the suite: declaringobjectproduced 26 refusals → 48 failing tests across 8 files of@objectstack/metadata-protocol's own suite, every one fromsecurity-owd-unsetand nothing else. Not stale fixtures —METADATA_CREATE_SEEDS.object, the spec's authoritative minimal create body, is{ name, label, pluralLabel, fields: {} }with nosharingModel. The platform's own runtime create door emits exactly the shape the rule refuses. The refusal is arguably correct (os buildhas rejected it since ADR-0090 D7), which is precisely why it is a rollout (#4001) and not a wiring fix. Its repair sites —packages/spec/src/kernel/metadata-create-seeds.ts+ 8 metadata-protocol test files — are ⛔ outside this charter.2.
permission/bookneed a snapshot the gate does not build.RuntimeStackContextcarriesobjectsand nothing else, so the three cross-collection rules compare against a collection that is absent. The per-write verdict is not a narrower whole-stack verdict, it is a different and wrong one: with one permission set in the snapshot, every detail object the tenant's other sets grant reads as ungranted. Hence 38 vs 4. ExistingRUNTIME_NEEDS_FULL_SNAPSHOT/ #4463 P2 — a snapshot change in the protocol package, not aruntimeTypesedit. Independently confirmed: neither type has aTYPE_TO_STACK_KEYentry, so declaring either fails the wiring guard's every runtime-gated type maps to a stack key case.3. No partial slice. The two ADR-0091 seed rules are genuinely ready (self-contained on
stack.data[], zero trips, cross as a whole sub-family) — left for the rollout card so the block crosses in one decision.position/appforsecurity-role-wordis not available at any point: that rule judges six collections, so wiring two of them splits one rule id across the wall — a door where a position namedsales_roleis refused and an object namedsales_roleis waved through. That is the #7220 failure this table already refuses to build.The false claim, replaced
- Coverage — 1 of 13, reproduced exactly.
object-posture-gate.ts(141 lines) reads exactlysharingModel+externalSharingModelthrough a localOWD_WIDTH, neverfields/permissions/books/data. Its R1 (env-tighten-only) maps to no lint rule, so it is not coverage in the other direction either. - Double-reporting was imaginary.
saveMetaItemrunsassertRuntimeAuthoringRules(422,protocol.ts:10105) beforerunAuthoringGate(403,protocol.ts:10146), and both refuse by throwing — the first to fire ends the write, so an author earns one refusal either way. Where they overlap the two are complementary: the posture gate's R2 additionally catches an external dial wider than an unset internal, where this block reportssecurity-owd-unsetinstead. This is the answer to the ruling's double-reporting question, and it needs no suppression logic — a documented note, as permitted.
Latent defect found and fixed
TYPE_TO_STACK_KEYmappedseed → 'seeds'. A stack has noseedskey — seeds live ondata, which is what both ADR-0091 rules read. Inert, not harmless: the wiring guard asks only that a declared type has a mapping, never that it names a key some rule reads, so it would have stayed green while the gate built{ objects, seeds: [item] }, every rule saw nothing, andrulesRunreported them as having run (#4449, one surface over). Corrected; inert today since nothing declaresseed.#7815 layer-sensitivity — measured, nil
validateSecurityPosture(stack, opts?)accepts exactly one option,nowMs; there is nolayerparameter to mis-set. The registry adapter isrun: (stack) => validateSecurityPosture(stack)— no options passed at all. The rules judge sharing posture, permission bits, vocabulary and seed-grant lifecycle; none resolves a predicate binding root. ⛔ #7815 untouched.Gates
gate result pnpm --filter @objectstack/lint run test71 files / 1929 tests pass (was 70/1922) pnpm --filter @objectstack/lint run typecheckclean pnpm --filter @objectstack/metadata-protocol run test72 files / 1066 tests pass against rebuilt lint dist — the behaviour-neutrality proof (48 failures with the move in, 0 with what landed) eslint --no-inline-config, 3 touched filesclean CI on #7886 in_progress— not polled, per dispatchNo suite pin weakened.
toHaveLength(16)and the wiring test's surface block are untouched and green — this PR changes no rule behaviour and no surface. New assertions name rule id and severity, never a bare count.⛔ Untouched:
plugin-security/**,metadata*/**,spec/src/**(read for measurement only); #7443 stayspm:on-hold—AUTHORING_SURFACESunchanged, no third value proposed;content/docs/releases/.
Generated by Claude Code
- Coverage — 1 of 13, reproduced exactly.
- added a commit that references this issue
on Aug 12, 2026 Review verdict: ACCEPT — PR #7886, the ruling's third branch (measurement +
surfaceReasonhonesty; no surface move).Verified against GitHub, not the report: 4 files, all
packages/lint/**+ one real patch changeset — nodocs/adr/**, no skill roots, nocontent/docs/releases/**, nopackages/spec/src/**. First line isPart of #7576, correctly — merging must not close this card while the remainder escalates. No suite pin weakened (toHaveLength(16)and the wiring test's surface block untouched); new assertions name rule id + severity. Behaviour-neutrality is consumer-proven:@objectstack/metadata-protocol's own 1066-test suite green against a rebuilt lint dist, versus 48 failures with the surface move in.Three things acknowledged in the open:
- The dispatch's load-bearing premise was corrected by measurement, which is the outcome the brief asked for. The PM premise read "the runtime plumbing can run rules over non-object collections"; the measured truth is narrower — the plumbing reaches them (
allowRuntimeCreate+ generic snapshot builder) but cannot carry the sibling collections three rules compare against (RuntimeStackContextholdsobjectsonly; 38-vs-4 on the shipped corpus). The corrected premise is inherited by the escalation card, not the original. runtime-gate.ts(seed → 'data') was outside the claim's declared file surface — accepted as a disclosed, in-lane, inert-today addition with its own measurement and pin, per the disclose-don't-absorb rule.- The double-reporting question is answered structurally (
assertRuntimeAuthoringRules422 throws beforerunAuthoringGate403; one refusal either way), with no suppression logic — as the dispatch permitted.
Remainder escalation (PM's side, in progress): the surface move is a strictness-rollout-shaped program whose repair sites span
packages/spec/src/kernel/metadata-create-seeds.ts+ 8@objectstack/metadata-protocoltest files (theobjecthalf, #4001 pattern) and theRUNTIME_NEEDS_FULL_SNAPSHOTgap already tracked as #4463 P2 (thepermission/bookhalf), with the ADR-0091 seed pair as the ready first slice. Filing it as a fresh unassigned card for triage to grade and route (it is cross-lane); this card closes when #7886 lands, pointing at that successor.Landing: CI convergence, ready-flip and queueing are this seat's — proceeding.
Generated by Claude Code
- The dispatch's load-bearing premise was corrected by measurement, which is the outcome the brief asked for. The PM premise read "the runtime plumbing can run rules over non-object collections"; the measured truth is narrower — the plumbing reaches them (
Landed and closing: PR #7886 merged via the queue as
fc87586. Post-merge verification by content onorigin/main: the measuredsurfaceReasontext present,seed → 'data'stack-key correction present,validate-security-posture.runtime-surface.test.tsintact at 239 lines.Closing this card as completed with a pointer, not as fully done on its own terms: the PR was deliberately
Part of #7576— what this card measured (the false coverage claim) is fixed and the measurement is now executable in the suite; the surface move itself is escalated as #7891 (unassigned, awaiting triage grading/routing — it spans spec semantic face + metadata-protocol + this lane, with #4463 P2 as the snapshot half). One thing, one open dispatch entry: keeping this card open in parallel with #7891 would give the same remainder two queue entries.pm:dispatchedstripped on close.
Generated by Claude Code
Found while implementing #7503 (PR #7574, the
controlled_by_parent-without-relation lint rule). Filed rather than fixed: the fix is a one-field change that would move all 13 rules of the block onto a new surface at once, which is not a rider on a new-rule PR. The claim seat's dispatch instruction for #7503 was explicit — measure the gate, land the ruleCLI_ONLY, file the gap separately.The claim
validateSecurityPostureis registered once, as a block, inpackages/lint/src/authoring-rules.ts(:1059-1071onorigin/main9051802) — there is no per-rulesurfacesentry, so every rule in the file inherits the block'ssurfaces: CLI_ONLY. ItssurfaceReason, verbatim:That is a claim about coverage. It is not a reading of the gate.
What the gate actually does
packages/plugins/plugin-security/src/object-posture-gate.ts— the whole gate is 127 lines, andobjectPostureGateimplements exactly two rules, both stated in its own header:sharingModel/externalSharingModelpast the packaged declaration.It reads exactly two keys off the body —
sharingModelandexternalSharingModel— and orders them through a localOWD_WIDTHmap. It never readsfields,actions,permissions,booksordata.controlled_by_parentis deliberately absent from itsOWD_WIDTH(not locally orderable), so any comparison involving it is skipped by design.The measurement
Mapping the gate against the 13 rule ids the block now carries (12 on
main, 13 with #7503's):objectauthoring gate?security-external-wider-than-internalsecurity-owd-unsetsharingModelto be setsecurity-owd-aliaswidthOf()returnsundefinedfor a non-canonical value, so the comparison is skipped, not refusedsecurity-controlled-by-parent-no-relation(#7503)fieldssecurity-master-detail-ungrantedobjectbodysecurity-private-no-readscopesecurity-wildcard-vamaobjectbody at allsecurity-anchor-high-privilegesecurity-fls-unqualified-keysecurity-role-wordsecurity-book-audience-unknown-setobjectbodysecurity-grant-expired-at-authoringdata[]security-delegation-missing-reasondata[]1 of 13. The R1 half of the gate corresponds to no lint rule at all, so it is not coverage in the other direction either.
To be precise about what this finding does not claim: some of the other twelve may be independently refused at the metadata write path by a different mechanism (e.g.
object.zod.ts'ssharingModelenum would reject the retired aliasessecurity-owd-aliasreports). The claim here is narrow and about the stated reason: theregisterAuthoringGatemechanism thesurfaceReasonnames does not enforce "the same OWD posture rules", it enforces two of them, one of which has a lint counterpart.Why it matters, and the evidence it is not theoretical
#7503 is the proof. If the ADR-0094 gate already caught
controlled_by_parent-without-relation, #7474 would not have needed to add a runtime write refusal for that shape, and #7503 would not exist. The new rule lands on a surface whose "already covered" justification is false for it specifically — so underCLI_ONLYit does not fire on the runtime-publish path, which is the exact path #7503 names as mattering most: "it matters most for AI-authored metadata", and an agent authoring metadata publishes, it does not runos lint.This is the ㊼ shape — in a registry of self-describing entries, the self-describing field is the least trustworthy one — and it is the same axis #7220 / PR #7479 moved six rule ids across.
Suggested shape (not a decision, and deliberately not taken in #7574)
The mechanical fix is to give the block
surfaces: CLI_AND_RUNTIMEwithruntimeTypesnaming the metadata types it judges. Two reasons that is its own card:runtimeTypesentries — or a split of the block into per-collection registrations — before that is even correct. A block registered forobjectonly would still not run the eight.packages/lint/src/authoring-rule-wiring.test.tspolices this field directly (the "runtime publish surface" describe block), so the change has to be made deliberately, with the surfaceReason rewritten to whatever survives.surfacesaxis itself to N surfaces; this one is about a single block's factualsurfaceReasoninside the existing two-value axis.AUTHORING_SURFACESandCLI_AND_RUNTIMEneed no change for this.Pointers
packages/lint/src/authoring-rules.ts:1059-1071— the block registration and itssurfaceReasonpackages/plugins/plugin-security/src/object-posture-gate.ts— the gate, in fullpackages/lint/src/authoring-rule-wiring.test.ts— the "runtime publish surface" guardsharingModel: controlled_by_parentwith nomaster_detailrelation is statically detectable and unreported #7503 / PR feat(lint): reportcontrolled_by_parentwith no relation to derive from (#7503) #7574 — the rule that made the gap measurable;assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 — the runtime refusal that proves itviews[]visibility-predicate rules are CLI-only — a Studio/REST/MCPviewwrite bypasses all of them; if they move to runtime-publish, they must move together #7220 / PR feat(lint): move the views[] visibility-predicate family to the runtime publish gate (#7220) #7479 — the same axis, six rule ids, hours earlier