Skip to content

lint(security-posture): record which intakes can reach security-owd-alias — measured, annotated, pinned - #16513

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-16109-security-owd-alias-reachability
Sep 7, 2026
Merged

lint(security-posture): record which intakes can reach security-owd-alias — measured, annotated, pinned#16513
baozhoutao merged 2 commits into
mainfrom
claude/issue-16109-security-owd-alias-reachability

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #16109

What this PR is

The card's primary ask was a measurement: which @objectstack/lint intakes can hand validateSecurityPosture a sharingModel that never passed ObjectSchema.parse? Triage (comment 5556566227) ordered the branches: no such intake, retire the dead branches; such an intake exists, do NOT retire, annotate the rule with its intake and update the coverage table's wording. The measurement found intakes, so this PR takes the annotate branch and changes no behaviour: the rule source diff is comment-only (proved below), plus a pin test that keeps the measurement honest. packages/spec's two enums are untouched.

Step 1 — the measurement (branch b8c72ac052, base c383352cb7)

Every leg was taken in one run against the freshly built dist of spec, formula, sdui-parser and lint (pnpm --filter '@objectstack/lint...' build, exit 0), for each of the four OWD_ALIAS_FIX keys (read, read_write, full, public). The os lint leg was additionally taken end to end with the CLI's real loader: loadConfig (bundle-require, no parse) from packages/cli/src/utils/config.ts on a raw objectstack.config.ts, then exactly the call lint.ts makes.

door what reaches the rule security-owd-alias? role
defineStack(x) (strict default) — every os init config, hence os validate / os build / os lint on such a config nothing: defineStack validation failed … objects.0.sharingModel: Invalid value at load no positive control (parses)
os validate / os compile schema step (ObjectStackSchema.safeParse) on a RAW config refused invalid_value at objects.0.sharingModel; compile stops before any rule no positive control
saveMetaItem (Studio / REST meta / MCP) — the runtime publish gate refused 422 invalid_value at sharingModel by getMetadataTypeSchema('object'), which runs BEFORE runRuntimeAuthoringRules (protocol.ts, "Placed immediately after the schema check") no positive control
a pre-D4 stored sys_metadata sibling in the gate's context universe read / read_write fold to canonical on rehydration (applyConversionsToStoredItem, full chain incl. retired); full / public come back as authored — and either way the finding is produced in BOTH baseline and candidate passes and cancels in the diff no control
os lint on a RAW object-literal config loadConfig returns the default export as authored; os lint never parses (content/docs/deployment/cli.mdx, lint.ts); owd-legacy-read-aliases is retiredFromLoadPath, so normalizeStackInput leaves the alias intact (0 conversion notices) yes — error at objects[0].sharingModel, hint names the canonical value bypassing intake
defineStack(x, { strict: false }) then os lint parse skipped; alias intact yes bypassing intake
check:doc-security-posture (packages/lint/scripts/check-doc-security-posture.mjs, wired in lint.yml) statically evaluated ObjectSchema.create({...}) literals from docs and skills, never parsed; its own --self-test asserts the alias value is flagged — the gate ran green here: "self-test: flags the measured defect (owd-unset) and the alias value at the right page line" yes bypassing intake, CI-wired
runRuntimeAuthoringRules / validateSecurityPosture called directly (both exported) item handed in unparsed yes (objects.tier_owd.sharingModel) bypassing intake (public API)

Re-derivation of the card's own hotcrm reading: the first row is it. On a defineStack-authored app the alias never arrives, and the sibling security-owd-unset branch fires on the byte-adjacent removal — the card was right about that door and wrong that it is the only door.

The four yaml mentions under packages/lint/src triage noted are prose (object-graph.ts: "a YAML list item left empty is null") and three tests; there is no yaml reader in the package. runRuntimeAuthoringRules's only production caller is metadata-protocol's evaluateRuntimeAuthoringGate, which sits behind the schema step above.

Step 3 — what changed

  • packages/lint/src/validate-security-posture.ts — comment-only: the header table row marks the rule "UNPARSED intakes only", a new ## Intake section carries the table above, and both alias branches (sharingModel, externalSharingModel) carry a one-line pointer. Code with comments stripped is byte-identical to main (measured: identical, 18166 chars both sides).
  • packages/lint/src/authoring-rule-input-tier.test.ts — a describe block pinning every leg with its parsed-door control: defineStack refuses each alias; the schema step refuses each alias; os lint's call fires with the canonical fix-it in the hint (0 conversion notices); strict: false fires; the canonical value stays silent; getMetadataTypeSchema('object') refuses each alias; a direct runRuntimeAuthoringRules call fires; a stored sibling folds (read, read_write) or survives (full) and cancels in the diff either way.

Why not the retire branch: retiring the branches would also have to delete the check:doc-security-posture self-test that asserts the alias verdict, and would take the only fix-it the os lint pre-flight gives a raw config carrying a retired spelling.

Re-route flag for the PM (triage's escalation clause)

Branch 3 taken, so per triage the card becomes "update the coverage table's wording", cross-seat. objectstack-ai/hotcrm is outside this session's scope; nothing was opened there. Suggested wording for hotcrm #1586's step-3 table row (the seat that owns it should confirm the row's current text):

security-owd-alias (error) — applies to unparsed intakes only: os lint on a raw object-literal config, defineStack(x, { strict: false }), the check:doc-security-posture docs gate, and direct API calls. On a defineStack-authored app such as this one the value is refused earlier by ObjectSchema's closed sharingModel / externalSharingModel enums (invalid_value at config load, exit 1; ADR-0090 D4 / D11) — the platform gate to credit for retiring a local alias assertion is the spec enum, not this rule id.

Tests and gates (all on b8c72ac052)

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/authoring-rule-input-tier.test.ts src/validate-security-posture.test.ts src/validate-security-posture.runtime-surface.test.ts src/rule-id-barrel-exports.test.ts src/authoring-rule-wiring.test.tsTest Files 5 passed (5), Tests 191 passed (191); os-verify-lock: VERDICT command-exit 0.
  • pnpm --filter @objectstack/lint typecheck (tsc plus check:test-typecheck over tsconfig.test.json, whose include is src/**/* so the new pins are compiled) — VERDICT command-exit 0.
  • Ablation, on the committed tree, restore by git checkout HEAD -- path, hash equal to the HEAD blob afterwards (d616b72f…): green leg 35/35; disabling the sharingModel alias branch alone (marker on disk 1, anchor 0) — red, 4 INTAKE pins fail on the missing fix-it; disabling both sharingModel alias branches (the retirement shape; marker 2, anchors 0) — red, 6 INTAKE pins fail. A first ablation attempt that disabled only branch 1 while the pins held rule id and path alone stayed green: the sibling "not canonical" branch emits the same id at the same path. That reading is why the pins now hold the fix-it hint.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 45 commands; all 45 ran, dispatch-gates --ran: 45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED (43 exit 0). Two answered PREREQUISITE NOT MET exit 3 because they read a whole-repo build (check:dual-build-cjs-loads, check:type-check-debt) — NOT MEASURED here, declared to CI. pnpm check:nul-bytes OK (8099 files). pnpm --filter @objectstack/lint run check:doc-security-posture green.
  • Declared narrowing. turbo ls --affected against the base lists 54 packages (every consumer of @objectstack/lint). Only @objectstack/lint was tested locally: the runtime source diff is comment-only (stripped-comment source identical to main), so no downstream consumer can observe a behaviour change; CI runs the full affected set.
  • Changeset: none — comments and tests only, nothing published changes; skip-changeset applies.

Generated by Claude Code

…alias`

`sharingModel` / `externalSharingModel` are closed enums (ADR-0090 D4 / D11),
so on every door that parses before the registry runs the alias branches are
unreachable by construction. Measured: `defineStack`, the `os validate` /
`os compile` schema step, and `saveMetaItem`'s object-schema step all refuse
the alias first; `os lint` on a raw object-literal config, `strict: false`,
the docs gate and a direct call hand it to the rule, which fires.

Annotates the rule's docblock and both alias branches with that intake
table, and pins every leg (with its parsed-door control) in
`authoring-rule-input-tier.test.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
The sibling non-canonical branch emits the same id at the same path; the
canonical replacement in the hint is what the alias branch alone adds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 1 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via validateSecurityPosture (symbol, a top-level function))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via validateSecurityPosture (symbol, a top-level function))
  • content/docs/releases/v17.mdx (via validateSecurityPosture (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5eccf92577490da8a4ee82285fcdcb5b876c0e1packageMentionDocs.

Which tree this was computed on

This run read content/docs from 72392eca144e52ae0260168e37c6d7d08497705e — the merge of head b8c72ac05256f5318ed79cd7fee5ac28f200eeec into base a5eccf92577490da8a4ee82285fcdcb5b876c0e1, which is what actions/checkout gives a pull_request run. Not the PR head.

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

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

node scripts/docs-audit/affected-docs.mjs --json a5eccf92577490da8a4ee82285fcdcb5b876c0e1

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: security-owd-alias cannot fire through defineStacksharingModel is a closed enum that refuses every alias the rule exists to name

2 participants