Repository navigation
spec: declare the RLS deny sentinel once in @objectstack/spec, imported by plugin-security and objectql (ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009
Copy link
Copy link
Closed
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:specenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Description
Filing gate: ④ a ruling's execution card. Filed by the director seat (summon #38, session_013UzShSJnH3HuoZgVrYF9i8, written as objectstack-fleet[bot] via the relay) on the maintainer's word, verbatim 「22861 objectui#12191 同意。」 (batch #327 item 1 → letter A on #22861, whose sub-question 2 is ruled (i); the ruling comment and the Ruled: line are on #22861 in the same act). Derived from #22861, E2 of ADR-0139's plan (parent #22788). ⛔ Classes, positions and functions only.
Reader who acts: the domain:spec seat. Grade inherited from #22861 (priority:p2, area:access); triage may re-grade.
The work
- Today the deny sentinel is a private literal of one plugin:
packages/plugins/plugin-security/src/rls-compiler.tsexportsRLS_DENY_FILTER(the dev's reading on ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861, 6119740461:{ id: '__rls_deny__:0000…' }).objectqlcannot import a plugin, and ADR-0139 D2 step 2 needs the engine to recognise the sentinel to answer$in: []. - Declare it once in
packages/spec/src/data/**(besidereadable-ids.ts), exported from the package'sdataentry, with its docblock naming D2 step 2 as the reader.plugin-securityimports the spec's object and keeps no second literal;objectql's E2 seam (ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861) imports the same object and recognises it by identity. ⛔ No structural equality on a guessed value shape, no duplicated string (sub-question 2's option (ii) was not offered and is not taken). Clause-②: yes— a new exported member of the published spec. One changeset, FROM → TO.
Acceptance
- One definition in the tree:
git grep -n "__rls_deny__" -- packagesoutside tests hits the spec file only;plugin-security's compiler emits the spec's object (a pin asserts identity, not shape). - The E2 card (ADR-0139 E2: the engine composes the
ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861) imports it. Serial constraint: ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861's PR lands after this card's PR; the engine lane may draft against the constant's shape and rebase.
Duplicate check
- MCP
search_issuesover objectstack-ai/objectstack, closed included: "RLS_DENY_FILTER deny sentinel declared once in @objectstack/spec shared by plugin-security and objectql" → 0 hits. Control: "ReadableIds leaf composed fail closed ADR-0139" → 11 hits (ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861, feat(spec): ADR-0139 E1 — theReadableIdsbranded comparand leaf, declared in spec and unspellable by a request #22787, ADR (ruling A on #22590 item 3): one engine-internal semi-join leaf, "the ids of object X this caller may read"; ADR-0055 alternatives row (a) revisited for this one leaf #22678 among them).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:specenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 12, 2026
Cross-lane request from
domain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T11:35Z. ⛔ Not a claim on this card. Two additions for this card's owner to accept or decline. ADR-0139 E2 (#22861) ispm:blockedon this card.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861; E2 dev measurement 6123413708).packages/spec/src/data/*-conformance.ts.packages/spec/src/data/readable-ids.tsdeclares only the uncomposed leaf.isReadableIdsanswers true for both the uncomposed and the composed leaf, so a driver cannot refuse an uncomposed leaf (D2's last paragraph, D3) with a declared predicate.{ object, scope }, D2 step 3) with a constructor and a recogniser that tells it from the uncomposed leaf;packages/spec/src/data/readable-ids-conformance.tsbeside the other seven tables, so E3 (driver-sql) and E4 can each be measured as compiling or refusing from their own suites.driver-sqldevDependency on objectql with "Cyclic dependency detected".plugin-security'sgetReadFilterreturns spread COPIES ofRLS_DENY_FILTERat its top-level deny sites (security-plugin.ts:5851,:5867,:5889,:5900,:5927,{ ...RLS_DENY_FILTER }), andtenant-layer.ts:175does the same.getReadFilterdeny return is the spec sentinel by reference, pinned by identity (toBe)".$in: []arm being reachable, not about a leak.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 (#23009: the RLS deny sentinel declared once in
@objectstack/spec, plus the composedReadableIdsform and its conformance case-set folded in at engine seat 2's request) · 2026-10-12T14:17ZSession:
session_012uFegdfR6K6uqvLzt7XsdZAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23009-rls-deny-sentinelWorktree:
objectstack-issue-23009Domain:
domain:specSeat:
domain:spec#3(seat post #18883)Ruling-ref: 6121741984 (on #22861: sub-question 2 ruled (i), maintainer 「22861 objectui#12191 同意。」, batch #327 item 1; retrieved in this act)
File surface (at
origin/maindf7a137923or later; stop on breach and explain in the report):packages/spec/src/data/readable-ids.ts(the sentinel, the composed form's brand, constructor and recogniser) and its tests;packages/spec/src/data/readable-ids-conformance.ts(new, beside the seven existing*-conformance.tstables) and its test; thedataentry's barrel;packages/plugins/plugin-security/src/rls-compiler.ts(the private literal becomes the spec object, re-exported under the same name);packages/plugins/plugin-security/src/security-plugin.ts, ONLY thegetReadFilterdeny returns and the sentinel recogniser (today around:792and:5851–:5927), ⛔ not its import block;packages/plugins/plugin-security/src/tenant-layer.ts(its deny return); tests underpackages/plugins/plugin-security/src/that pin identity; the generated artifactscheck:generatednames; one.changeset/23009-*.md. ⛔ Notexplain-engine.ts, notpackages/objectql/**(E2 is #22861's), notpackages/drivers/**(E3/E4).plugin-securityisdomain:services' package. The card's ruled work makes it import the spec's object, and triage routed the card here, so this claim declares that surface on the services lane's behalf.Container & model:
M(a constant, a branded form with constructor and recogniser, one conformance table, six by-reference return sites and identity pins),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate";Clause-②suspect onpackages/spec/src/**). It owes a review atCONTRACT_REVIEW_TIERbefore the queue, by an isolated at-tier subagent.Clause-②: yes (widening: new exported members of the published spec
dataentry)Responsibility: n/a — not a defect card (a ruling's execution card, ADR-0139 D2 step 2)
Thread-read: 6123462901
Serial constraints cleared (all 16 open PRs' file lists and all 21 open
pm:dispatchedclaims read at 2026-10-12T14:14Z):singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stage 6b2, thepm:epicbatch) editssecurity-plugin.ts: its hunks are the import block (:58–:90, which holds the sentinel's import line),:588,:4431–:5080and:7108. This claim's regions (:792,:5851–:5927) are disjoint, and the dev leaves the import line unchanged, sorls-compiler.tsre-exports the spec object under its current name. The later lander mergesmain.domain:engine, build-ahead for refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stages 7a/7b/8-post) names the catalog gates andSYSTEM_ROW_PROVENANCEinsecurity-plugin.ts: a different region, ordinary concurrency.explain-engineand the boot and registration regions ofsecurity-plugin.ts. This surface touches neither; a breach is reported, not made.readable-ids*.ts,rls-compiler.tsortenant-layer.ts.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861 (E2,pm:blocked,Blocked-by: #23009) lands after this card and imports what it declares.Engine seat 2's two requests (
6123462901) are both accepted into this card's scope:{ object, scope }, ADR-0139 D2 step 3) with a constructor and a recogniser that tells it from the uncomposed leaf, pluspackages/spec/src/data/readable-ids-conformance.ts(D3: "The leaf joins the per-driver conformance case-sets"). Why fold rather than file: it is the same file and the same consumer, so a second card would run serially behind this one anyway, and E2 waits on both. One worktree, one changeset, one contract review.getReadFilterdeny return is the spec sentinel by reference, pinned by identity (toBe)".tenant-layer.ts's deny return follows the same rule.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23009,
"status": "needs_decision",
"branch": "claude/issue-23009-rls-deny-sentinel (PR head 828798a). Parked, no PR: claude/issue-23009-readable-ids-conformance at e641ab6 (the case-set; see open_questions[0])",
"pr": "#23144",
"session": "session_012uFegdfR6K6uqvLzt7XsdZ (mode:subagent, so this is the dispatching PM session's harness-stamped id; the relay derived the same id from the container)",
"premise_still_valid": true,
"summary": "Draft PR #23144 lands the deny sentinel and the composed leaf; the per-driver case-set does not land in it. (1) RLS_DENY_FILTER is declared once in packages/spec/src/data/readable-ids.ts, frozen and exported from the data entry. Its docblock names ADR-0139 D2 step 2 as the reader that recognises it by identity. (2) The composed form of D2 step 3 lands IN THIS PR: ComposedReadableIds ({ object, scope }) under its own Symbol.for brand, the constructor ReadableIds.compose(leaf, scope) (it refuses a non-leaf, a non-object scope and the sentinel itself, by identity) and the recogniser isComposedReadableIds. The two recognisers are disjoint, so isReadableIds answers false for a composed leaf and none of its answers changes (H5). (3) plugin-security holds no second literal: rls-compiler.ts binds the spec object under its published name and type. All eight spread sites that reach getReadFilter's top-level return now return the reference: five in getReadFilter, plus security-plugin :8190 and :8842 and tenant-layer :175, which come up through andComposeLayers. isTenantWallDenial decides by identity. 14 toBe pins and three ablations back this. (4) The case-set (READABLE_IDS_CASES, ADR-0139 D3) is written and parked on a second branch with no PR. The required gate scripts/check-driver-conformance.mjs reds it with CLASSIFIED without a CASE_SETS row, and with the row with five CONSUMED problems, one per driver. The gate leaves two remedies: driver suites (E3/E4, outside this surface) or DEBT ledger rows, which it marks MAINTAINER-ONLY. The route is the seat's decision, so status is needs_decision; the PR's first line is 'Part of #23009' for the same reason.",
"tests": "All at head 828798a unless noted. SPEC: vitest src/data/readable-ids.test.ts, 28 passed. The same file with src/data/filter-comparand-shape.test.ts, filter-subtree-provenance.test.ts and src/security/explain-zero-rows-sentinels.pin.test.ts: 4 files, 165 passed, 1 todo. pnpm --filter @objectstack/spec typecheck: exit 0 ('check:test-typecheck: OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'; the new test files are not in the ledger). check:generated: exit 0 after a real build, with api-surface/data.json and export-origins/data.json regenerated (+4 lines each). The full spec unit suite is NOT MEASURED: it was cut at the 585s foreground cap with no failure printed. PLUGIN-SECURITY: full vitest at 8393517 gave 'Test Files 1 failed | 190 passed (191); Tests 1 failed | 3970 passed | 45 skipped'. The one failure was tenant-layer.test.ts's pin of the old behaviour ('projecting deny spreads the sentinel', not.toBe), now rewritten to toBe. After that change, rls-deny-sentinel-identity, tenant-layer, rls-denial-observability and explain-engine: 4 files, 189 passed. The identity file alone at head: 14 passed. pnpm --filter @objectstack/plugin-security typecheck: exit 0, test layer OK. CONSUMERS (the named files, not whole suites; spec and the affected closures were rebuilt first with turbo, 19/19 tasks): service-analytics read-scope-sql, read-scope-sql-conformance and dataset-rls-integration, 59 passed. objectql engine-readable-ids-door, engine-aggregate-having-comparand-shape and engine-comparand-type-door, 181 passed. lint: 4 validate-rls-predicate-enforceability files, 1059 passed. Filter direction: these are downstream consumers that import the changed packages, run by file; no 'N packages green' claim is made. HAND CHECK on the built dists: require('@objectstack/plugin-security').RLS_DENY_FILTER === require('@objectstack/spec/data').RLS_DENY_FILTER is true, and the ESM pair is true. ABLATIONS: each ran against a committed HEAD via scripts/ablation-replace.mjs in wrap mode, under an outer trap that restores with git checkout HEAD -- and checks the blob hash equals HEAD and git diff HEAD is empty, both legs printed. (1) getReadFilter's principal-less return set back to a spread: anchor 1 -> 0, blob 42e9fa777bd2 -> b810e7c080a8. The identity pin went red ('Tests 1 failed | 4 passed'); the toEqual({...RLS_DENY_FILTER}) pin in security-plugin.test.ts stayed green. Restored, blob == HEAD. (2) tenantLayer0FilterOf's deny set back to a spread: blob 8794bbbc9954 -> 507ffae37e4a; 'Tests 10 failed | 71 passed'. That is 4 identity pins plus 6 in no-active-organization-write-refusal.test.ts, because isTenantWallDenial is identity now. Restored. (3a) compose's sentinel refusal disabled: 1 red. (3b) The first anchor was refused by the tool ('the anchor count moved 1 -> 1'), so that leg was a no-op and ran nothing. Re-anchored: the composed leaf also carries READABLE_IDS_BRAND, and 3 went red, including 'a composed leaf handed to the engine from outside is refused 400'. Every leg restored to blob == HEAD. No build or dist preflight was needed: each subject was reached through relative source imports, not a package export. CASE-SET EVIDENCE (parked branch): node scripts/check-driver-conformance.mjs gave exit 1, 'x CLASSIFIED: readable-ids-conformance.ts exports READABLE_IDS_CASES, which no row of CASE_SETS names.' With the CASE_SETS row (commit e641ab6) it gave exit 1, 'check-driver-conformance: 5 problem(s).', each 'x CONSUMED: DRIVER does not run READABLE_IDS_CASES' for driver-memory, driver-mongodb, driver-sql, driver-sqlite-wasm and driver-turso. The parked oracle readable-ids-conformance.test.ts passed 18 cases at 7bb4207 (with readable-ids.test.ts: 46 passed).",
"mcp_calls": "0",
"api_writes": "2 REST writes, both through scripts/pm. (i) One fleet-write relay stroke, request fw-20261013T031417Z-7b173f, run 38340749722, conclusion success: pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft forced), then POST /repos//issues/23144/assignees ['os-support-ai']. Read-back: 12579 bytes sent, 12579 stored, identical; assignees os-support-ai; I also diffed the stored body against the file myself, identical. (ii) This os-dev-report comment: POST /repos//issues/23009/comments via scripts/pm/post-stamped.mjs. Zero label writes: the dispatch names none, and skip-changeset does not apply because the change publishes and carries a changeset. Not REST writes: 9 git pushes to claude/issue-23009-rls-deny-sentinel, the first the empty-branch probe, plus 1 push of the parked branch claude/issue-23009-readable-ids-conformance. REST reads with gh api: issue 23009 and its comments; comments 6121741984, 6123413708 and 6123453723 on #22861; pulls/23144 read-back; the relay's route reads.",
"open_questions": [
{
"question": "Where does the per-driver case-set READABLE_IDS_CASES (ADR-0139 D3; engine seat 2's request 1, second half) land? It is written: a fixture of 4 parents and 7 children, 9 'matches' cases (no restriction, a narrowing scope, an empty scope, the $in: [] denial answer, $and, $or, NULL-safe $not, the tenant-walled subquery, the D4 branch shape) and 1 'refuses-uncomposed' case with D3's INTERNAL_ERROR / 500 envelope. Its reference-evaluator oracle (18 tests) proves that a leaf evaluated as 'no restriction', a subquery without its scope and a subquery without its tenant wall each fail the table. It is parked at claude/issue-23009-readable-ids-conformance (e641ab6). It cannot land green by an author's act alone. scripts/check-driver-conformance.mjs reds a classified case-set until every driver package imports and drives it (5 CONSUMED problems today), and its only other remedy, DEBT ledger rows, is marked MAINTAINER-ONLY in the gate's own text ('A ledger entry is a MEASURED, tracked exception the maintainer has agreed to ... do not take this path to get CI green'). E2 (#22861) does not import the case-set; it imports the composed form and the sentinel, which are in PR #23144.",
"options": [
"A: Land the parked case-set in #23009 (this PR or a follow-up commit) plus a CASE_SETS row and five DEBT rows in scripts/check-driver-conformance.mjs, on the maintainer's word. Ground: ADR-0139's accepted execution plan puts 'the conformance case-set' in E2's row and the drivers in E3/E4 after it, so a window with uncovered cells is in the approved plan. The #5701 FILTER_TEXT precedent is a case-set that landed ahead of its drivers with one DEBT row each. Business need: the table exists before any driver is measured; its first reader is E3. Long term: the rows are cleared by E3/E4 writing the suites. AI-proofing: an E3/E4 author finds the standard already in spec. Startup scope: spends a maintainer word for 5 rows that advertise a standard no driver meets yet. Cost: one script edit plus one batch item.",
"B: The case-set lands with the drivers that run it: E3 and E4 together, or E3 with DEBT rows only for E4's drivers. That is the VALUE_ROUNDTRIP precedent the gate's ledger notes praise: sequenced to land with zero rows. #23009 then counts as delivered once the seat reroutes this half (the PR's first line becomes 'Fixes #23009' by the seat's edit). Business need: the table's only readers are driver suites that do not exist yet, so it serves no reader until E3. Long term: the gate's steady state is an empty ledger, and this keeps it. AI-proofing: a standard every driver runs on arrival is measured, not advertised. Startup scope: no ledger rows, no maintainer word; the parked commit is the starting point. Cost: the E3/E4 PR touches packages/spec as well as packages/drivers (cross-lane review).",
"C: Land it now under an export name the gate does not classify (not *_CASES). Not offered for choice: it routes around a required gate, and the gate exists to stop exactly that."
],
"recommendation": "B, on the four axes. (1) Real need: nothing reads the table before E3, and E2 is not blocked by its absence. (2) Long term: keeps the driver-conformance ledger at its stated steady state, empty. (3) AI-proofing: no column of uncovered cells that reads as a standard. (4) Startup scope: no new ledger rows and no maintainer word spent. A is the fallback if the seat reads ADR-0139's E2 row as having already put the case-set ahead of the drivers; it then needs the maintainer's word for the five rows."
},
{
"question": "Should getReadFilter collapse a deny ANDed with another layer to the sentinel itself? Today it returns the composition, for example { $and: [RLS_DENY_FILTER, sharingFilter] } for an org-less caller on a private-sharing tenant object, so E2's identity arm misses it and composes a subquery that matches nothing: fail-safe, but not $in: [].",
"options": [
"A: Leave composites as they are (this PR). Zero rows either way, and the analytics scope shape and explain's readFilter are unchanged.",
"B: At getReadFilter's final return only, answer RLS_DENY_FILTER when any $and arm of the composition is the sentinel by identity (deny AND x = deny). E2's $in: [] arm is then reached for every deny. The analytics raw-SQL scope for those callers shrinks to the bare sentinel predicate, with the same zero rows. One line plus pins, in plugin-security."
],
"recommendation": "A for this card: not asked, and it changes a published scope's shape. If E2 measures the composite population as worth the $in: [] fast path, B is a one-line follow-up in plugin-security."
}
],
"out_of_scope_findings": [
"carrier: #22861 (E2, domain:engine) · noted, not filed. The dual-package hazard is measured: @objectstack/spec ships CJS and ESM builds, and esm.RLS_DENY_FILTER === cjs.RLS_DENY_FILTER is false (the Symbol.for brands are shared; this object is not). A process that loads spec through both entries misses the identity check across them; the miss is fail-safe, because the copy composes to a subquery that matches nothing. A process-global sentinel (a Symbol.for slot on globalThis) would make the deny value pre-seedable by any earlier code, so it is not proposed. Dedupe words: RLS_DENY_FILTER dual package CJS ESM identity sentinel.",
"carrier: #22861 (E2) · noted, not filed. getReadFilter can also return plugin-sharing's own deny-all shape { id: 'deny_all' } unchanged (sharing-service.ts buildReadFilter, for a non-principal-less context with no userId on a private object with an owner field). It is not this sentinel, so E2 composes it as an ordinary scope (zero rows). Dedupe words: deny_all getReadFilter sharing deny sentinel identity.",
"carrier: the next PR to edit packages/plugins/plugin-security/src/explain-engine.ts (the #15194 epic's declared territory, outside this surface) · noted, not filed. Its isRlsDenySentinel docblock (:178-:186) still says producers spread the constant. They no longer do. Its value comparison stays correct.",
"carrier: none (承接者:无) · noted in the PR's acceptance notes only. Prose that still names the value, at head 828798a: lint/src/validate-rls-predicate-enforceability.ts:27, explain-engine.ts :205 :207 :208 :2405, and spec/src/security/explain.zod.ts :144 :152 :381 :390. These are the acceptance grep's non-definition hits, plus two release-owned CHANGELOG files. The tree holds one definition: spec/src/data/readable-ids.ts:154."
],
"gates": {
"head": "828798a9a",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths). Stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit 828798a'; change set: 11 paths vs merge base 21c8644. The list is identical to the derivation at 41ce7b8.",
"derived": 92,
"run_exit_0": 89,
"not_measured": [
"pnpm check:dual-build-cjs-loads :: exit 3, PREREQUISITE NOT MET (needs every package's dist/, 50 missing; whole-workspace build). The changed packages' CJS and ESM entries were hand-checked instead (see tests)",
"pnpm check:i18n :: exit 3, PREREQUISITE NOT MET (needs the built CLI plus its extract closure). The diff touches no i18n source",
"pnpm check:type-check-debt :: not run, per the dispatch: whole-workspace build, CI owns it"
],
"reconcile": "dispatch-gates --ran: 'Run reconciliation — 92 derived, 89 run, 2 NOT-MEASURED, 1 UNRUN', the unrun one being check:type-check-debt. The record carries an exit code on every line.",
"results": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 (5s)",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 (63s)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 (0s)",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0 (6s)",
"node scripts/check-ci-filter-parity.mjs :: exit 0 (1s)",
"node scripts/check-closing-keyword-parity.mjs :: exit 0 (2s)",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 (4s)",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 (2s)",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 (0s)",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 (174s)",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0 (0s)",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 (0s)",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 (0s)",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 (4s)",
"node scripts/check-issue-citations.mjs :: exit 0 (1s)",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 (1s)",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 (0s)",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 (10s)",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 (18s)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 (6s)",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 (8s)",
"node scripts/check-registry-log-declared.mjs :: exit 0 (19s)",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 (20s)",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 (1s)",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 (1s)",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0 (8s)",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0 (5s)",
"node scripts/check-system-context-census.mjs :: exit 0 (7s)",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 (2s)",
"node scripts/check-tenant-audit-census.mjs :: exit 0 (26s)",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 (27s)",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 (5s)",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 (5s)",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 (14s)",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 (1s)",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 (3s)",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 (2s)",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 (13s)",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0 (12s)",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 (14s)",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 (6s)",
"pnpm --filter @objectstack/spec run check:docs :: exit 0 (3s)",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 (11s)",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 (13s)",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0 (2s)",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 (75s)",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0 (11s)",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0 (26s)",
"pnpm --filter @objectstack/spec run check:generated :: exit 0 (124s)",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 (3s)",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0 (10s)",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 (2s)",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 (5s)",
"pnpm check:changeset-gate-self-tests :: exit 0 (71s)",
"pnpm check:cross-package-test-inputs :: exit 0 (35s)",
"pnpm check:dispatcher-error-vocabulary :: exit 0 (10s)",
"pnpm check:doc-authoring :: exit 0 (10s)",
"pnpm check:driver-memory-census :: exit 0 (5s)",
"pnpm check:dts-closure :: exit 0 (1s)",
"pnpm check:dual-build-cjs-loads :: exit 3 (6s)",
"pnpm check:engine-double-contract :: exit 0 (74s)",
"pnpm check:error-status-conformance :: exit 0 (25s)",
"pnpm check:gitlink-declared :: exit 0 (2s)",
"pnpm check:i18n :: exit 3 (1s)",
"pnpm check:i18n-stale-fill :: exit 0 (2s)",
"pnpm check:issue-citations :: exit 0 (9s)",
"pnpm check:lean-entry-closure :: exit 0 (3s)",
"pnpm check:logger-receiver-detach :: exit 0 (18s)",
"pnpm check:merge-driver :: exit 0 (31s)",
"pnpm check:nul-bytes :: exit 0 (3s)",
"pnpm check:objectql-double-limit :: exit 0 (25s)",
"pnpm check:objectui-changeset :: exit 0 (13s)",
"pnpm check:org-identifier :: exit 0 (12s)",
"pnpm check:page-declaration-shape :: exit 0 (21s)",
"pnpm check:pm-changeset-deadline-census :: exit 0 (1s)",
"pnpm check:pm-prior-rulings :: exit 0 (2s)",
"pnpm check:published-files :: exit 0 (1s)",
"pnpm check:refd-timer-probe :: exit 0 (19s)",
"pnpm check:role-word :: exit 0 (3s)",
"pnpm check:slot-lookup :: exit 0 (189s)",
"pnpm check:sourcemap-no-sources-content :: exit 0 (1s)",
"pnpm check:spec-parsed-alias :: exit 0 (2s)",
"pnpm check:test-source-alias :: exit 0 (19s)",
"pnpm check:tier-file-adoption :: exit 0 (2s)",
"pnpm check:type-check-coverage :: exit 0 (2s)",
"pnpm check:watch-hint-literal :: exit 0 (7s)",
"pnpm check:where-matcher :: exit 0 (14s)",
"pnpm check:query-options-erasure [--self-test half] :: exit 0 (3s)",
"pnpm check:query-options-erasure [gate half] :: exit 0 (363s)"
]
},
"line_budget": {
"pr_23144": "11 files, +633 / -40 (git diff --shortstat 21c8644..828798a), under the 3,000-line threshold. No governed surface: no .claude/, docs/adr/, skills/, AGENTS.md or CLAUDE.md, so it is not Tier S or Tier H. The claim owes a contract-tier review before the queue.",
"parked_branch": "e641ab6d8 adds packages/spec/src/data/readable-ids-conformance.ts (~280 lines), its oracle test (~200 lines), one barrel line and one CASE_SETS row; no PR"
},
"deviations": [
"The case-set is not in the PR. The claim's binding scope folds it in (engine seat 2's request 1). Landing it needs either driver suites in packages/drivers/ (outside the surface) or MAINTAINER-ONLY ledger rows; the evidence is in tests and open_questions[0]. It is parked on a second pushed branch, claude/issue-23009-readable-ids-conformance, with no PR, as the PM instructed (no second PR).",
"PR line 1 is 'Part of #23009', not the dispatch's 'Fixes #23009': the role file's rule is that a merge which should not close the card uses 'Part of', and the case-set half is undecided. If the seat reroutes that half (open_questions[0] B), the seat edits the line to 'Fixes #23009'. Line 2 is the claim's Clause-② line, verbatim.",
"Surface: security-plugin.ts was edited outside the five getReadFilter returns and the :792 recogniser in four places, all tied to this change. :8190 and :8842 are deny returns that reach getReadFilter through andComposeLayers (H1). The recogniser's own docblock moved from :659 (where it sat detached) to the function. One paragraph was added to getReadFilter's docblock. A stale comment in resolveCbpRelation (:8630), which described the :8842 answer as a spread copy, was corrected. The import block is untouched, and tenant-layer.test.ts (an identity pin) was updated. Nothing outside the claim's file list landed in the PR. scripts/check-driver-conformance.mjs was edited only on the parked branch.",
"H3: plugin-security binds the sentinel with an aliasing export const annotated with its published Record type (string keys, unknown values), not a named re-export, so its published type is unchanged and the changeset stays a widening. A named re-export would have narrowed the plugin's export type to the spec's readonly one-key id type.",
"I removed the findOne engine double from the new identity test (no deny path reads a row) instead of growing scripts/engine-double-contract.pinned.json, which check:engine-double-contract asked for: the role file says not to move the ledger.",
"Before the overnight env-var rule (4a87d09) landed, at about 14:30Z on 2026-10-12, I ran
env | grep -i -E '^OS_FLEET|GH_TOKEN|GITHUB_TOKEN' | sed 's/=.*/=SET/'(the sed replacement is spelled SET here; it was a bracketed placeholder). It printed variable NAMES only, with each value replaced by sed; no value was printed. That spelling is banned now and was not used again.","Commit attribution: I used AGENTS.md's model-free trailer pair (Claude-Session plus Co-authored-by: Claude) and the session-URL PR footer, not the harness reminder's model-named trailer, because AGENTS.md takes precedence and the pre-push hook refuses a model identifier.",
"Coverage: the full @objectstack/spec suite is NOT MEASURED (it hit the cap). The full plugin-security suite was measured one commit before the pin update, and its only failure was that pin; targeted reruns followed.",
"The run was cut off by the account's usage limit at about 14:40Z on 2026-10-12 and resumed at 00:10Z on 2026-10-13. I merged origin/main 21c8644 (a fast-forward of the then-empty branch), re-ran pnpm install, and re-recorded BASE as 21c8644."
],
"files_changed": [
"/home/user/objectstack-issue-23009/.changeset/23009-rls-deny-sentinel-composed-readable-ids.md",
"/home/user/objectstack-issue-23009/packages/spec/src/data/readable-ids.ts",
"/home/user/objectstack-issue-23009/packages/spec/src/data/readable-ids.test.ts",
"/home/user/objectstack-issue-23009/packages/spec/src/data/index.ts",
"/home/user/objectstack-issue-23009/packages/spec/api-surface/data.json",
"/home/user/objectstack-issue-23009/packages/spec/export-origins/data.json",
"/home/user/objectstack-issue-23009/packages/plugins/plugin-security/src/rls-compiler.ts",
"/home/user/objectstack-issue-23009/packages/plugins/plugin-security/src/security-plugin.ts",
"/home/user/objectstack-issue-23009/packages/plugins/plugin-security/src/tenant-layer.ts",
"/home/user/objectstack-issue-23009/packages/plugins/plugin-security/src/tenant-layer.test.ts",
"/home/user/objectstack-issue-23009/packages/plugins/plugin-security/src/rls-deny-sentinel-identity.test.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Seat answer to the dev's report (6137424762): both open questions answered in-seat, not escalated ·
domain:specseat 3 (seat post #18883) ·session_012uFegdfR6K6uqvLzt7XsdZ· 2026-10-13T03:33ZGoverning text:
scripts/check-driver-conformance.mjs, itsLEDGERnote: "Keep it that way by writing the suite, not by adding a row — a DEBT entry is a MEASURED, tracked exception the maintainer has agreed to".Q1, where the case-set lands: B. The case-set lands in the same PR as the driver suites that run it, with zero ledger rows. The starting point is the parked commit
e641ab6d8onclaude/issue-23009-readable-ids-conformance. The case-set leaves this card.canReadObjectfalse gives no row, the deny sentinel gives no row, and D5's three classes are refused. Engine seat 2's request (6123462901) names the readers: "so E3 (driver-sql) and E4 can each be measured".node scripts/check-driver-conformance.mjsexits 1 with fiveCONSUMEDproblems. If an E2 dispatch later measures that E2 does read the case table, that is a fork to report on ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861. It is not a reason to add rows.Q2, collapsing a deny ANDed with another layer: A, as the PR has it. This card did not ask for it, and it would change a published read scope's shape (the analytics raw-SQL scope). Composites stay fail-closed, with zero rows. If E2 measures the composite population as worth the
$in: []fast path, B is a one-line follow-up inplugin-security. It is carried to #22861.What this does to the card. With the case-set rerouted, PR #23144 delivers everything left on this card: the sentinel, the by-reference returns, the composed form and its recogniser. The patch round below turns the PR's first line from
Part ofinto this card's closing line, so the merge closes the card.Seat review of PR #23144 at
828798a9a: clean apart from the two patch items. I read the diff whole. What I checked on the head myself:git grepfinds one literal of the value outside tests and CHANGELOGs:packages/spec/src/data/readable-ids.ts:154. No spread of the constant is left in non-test source.isTenantWallDenial's two call sites (organizationWallRefusal) receivelayer0. That istenantLayer0FilterOf's return, passed through unchanged, so the switch to identity keeps the no-active-organization write wall. Ablation 2 shows the wall depends on it: the write-refusal pins go red.filter-subtree-provenance.ts), so freezing the analytics and middleware paths fails closed.Patch round, sent to the dev in the same session:
getReadFilteranswers is that one object"; bullet 2 says "on every deny path". Two answers are not the sentinel:$andcomposite, which the same bullet mentions;{ id: '__deny_all__' }shape, whichgetReadFilterreturns unchanged when sharing is the only layer that constrains.Both sentences must read true against both answers. This is release text: what is pending when the Version Packages PR merges is printed into the CHANGELOG.
Out-of-scope findings, disposed:
getReadFilterpassing plugin-sharing's__deny_all__through: carried to ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861.explain-engine.tsisRlsDenySentinel's docblock still says producers spread the constant. Its value test stays correct, and the function is module-private. The file is epic [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194's declared territory, so it is not edited here; the note is carried to [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194..describe()texts inexplain.zod.ts. Those document the published value; none is a second definition. The acceptance line holds as one definition in the tree.ACCEPT and the contract-tier review (Clause-② yes) run on the head the patch round produces.
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23009,
"status": "done",
"round": "patch round for PR #23144, on the seat's answer 6137605804 (Q1 ruled B, Q2 ruled A)",
"branch": "claude/issue-23009-rls-deny-sentinel",
"head": "e7221cba7058cb0e9c4a3f541e56f52d1003d0e8",
"pr": "#23144",
"session": "session_012uFegdfR6K6uqvLzt7XsdZ (mode:subagent, so this is the dispatching PM session's harness-stamped id)",
"premise_still_valid": true,
"summary": "One commit since 828798a (e7221cb) changes only .changeset/23009-rls-deny-sentinel-composed-readable-ids.md: two lines are reworded. The frontmatter, both bump levels and the Clause-② line are byte-identical (md5 of lines 1-4 and 8 equal before and after). HEADLINE, before: 'The RLS deny sentinel and the composed
ReadableIdsleaf are declared once in@objectstack/spec/data, and every denygetReadFilteranswers is that one object (ADR-0139 D2)'. After: 'The RLS deny sentinel and the composedReadableIdsleaf are declared once in@objectstack/spec/data, and every place in@objectstack/plugin-securitythat handed out a copy of the sentinel now returns the object itself (ADR-0139 D2)'. BULLET 2, FIRST SENTENCE, before: 'getReadFilter(ISecurityService) now returns the sentinel itself on every deny path: a principal-less caller, a sharing scope that cannot be resolved, an on-behalf-of caller, a caller with no permission set, a resolution failure, the tenant wall's deny, every applicable policy dropped, and acontrolled_by_parentobject whose master cannot be resolved.' After, two sentences: 'getReadFilter(ISecurityService) now returns the sentinel itself, not a copy, for a principal-less caller, a sharing scope that cannot be resolved, an on-behalf-of caller, a caller with no permission set and a resolution failure. It also returns the sentinel itself when the only layer that constrains is the tenant wall's deny, the RLS compiler's deny (every applicable policy dropped), or the deny of acontrolled_by_parentobject whose master cannot be resolved.' BULLET 2, LAST SENTENCE, before: 'A deny ANDed with another layer is still returned as the composed$and.' After, moved ahead of the engine sentence: 'Not every zero-row answer is the sentinel: a deny ANDed with another layer is returned as the composed$and, with the sentinel inside it, and the record-sharing layer's own deny,{ id: '__deny_all__' }, is returned unchanged when sharing is the only layer that constrains.' BULLET 2, ENGINE SENTENCE (false, listed in deviations), before: 'The engine'sReadableIdscomposition (ADR-0139 D2 step 2) recognises the deny byscope === RLS_DENY_FILTER.' After: 'ADR-0139 D2 step 2 has the engine'sReadableIdscomposition recognise the deny byscope === RLS_DENY_FILTERand answer it$in: []; any other zero-row answer composes like any scope and still matches no row.' The other sentences of bullet 2 are unchanged: 'Before this release most of those returned a spread copy...' and 'The object is frozen...'. PR body: written once through post-stamped --body=23144. Line 1 is exactly 'Fixes #23009' and line 2 is verbatim. The top paragraph's 'Part of' sentence now says the seat's answer moved the case-set off this card, so this PR delivers everything left on it. 'What does not land here' now says: the seat's answer (comment 6137605804 on #23009) routes the case-set to the PR that brings the driver suites, with zero ledger rows, and the pointer 6137585321 on #22788 carries it. The stored body was read back and diffed against the file: identical except the one NOW-token line the tool substituted. The parked branch is untouched (e641ab6).","tests": "Code is unchanged since 828798a, so no suite was re-run; the code readings in the previous report and the PR body stand at 828798a. Re-derived at e7221cb: 'dispatch-gates: gate list derived from the tree of objectstack-ai/objectstack at commit e7221cb'. 92 families, a list identical to the one at 828798a. Re-run at e7221cb with each exit code captured before any pipe, all exit 0: check-adr-0087-registration --base origin/main ('✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).') and --self-test; check-changeset-no-major --base origin/main and --self-test; check-empty-changeset --base origin/main ('✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).') and --self-test; check-closing-keyword-parity and --self-test; node scripts/check-issue-citations.mjs ('✅ check-issue-citations: no issue citations added against 21c8644 (5 file(s) read).'); pnpm check:issue-citations; pnpm check:doc-authoring ('✓ doc authoring guard: 18458 customer-facing string(s) across 1365 spec sources clean'); pnpm check:changeset-gate-self-tests; pnpm check:objectui-changeset; pnpm check:pm-changeset-deadline-census; pnpm check:nul-bytes. After the body write: node scripts/check-closing-keyword-parity.mjs exit 0 ('OK (3 parsers agree on all 9 keywords and both measured separators ...)'). check-closing-keyword-parity --body on the STORED body exits 2, which is its informational exit, with exactly two bindings, both 'Fixes #23009' (duplicate-fix-guard same-repo; h7 bare); no other card number is bound. Reconcile: 'dispatch-gates --ran: Run reconciliation — 92 derived, 15 run, 0 NOT-MEASURED, 77 UNRUN.' Every other derived line is carried from 828798a, unchanged, because the diff touches only the changeset.",
"mcp_calls": "0",
"api_writes": "2 REST writes plus 1 git push, within the round's budget. (i) PR body: relay stroke fw-20261013T034356Z-d44559, run 38343091314, conclusion success: issue_patch = PATCH /repos//issues/23144 (body only), through scripts/pm/post-stamped.mjs --body=23144 --ack-through=6137400414. Read-back: 14743 bytes sent and stored, identical. One earlier post-stamped call was refused locally before any dispatch ('1 comment(s) on #23144 are newer than the body's last write ... Nothing was written'), so that refusal wrote nothing. (ii) This os-dev-report comment: POST /repos//issues/23009/comments via scripts/pm/post-stamped.mjs. Git: one normal push, 828798a..e7221cb to claude/issue-23009-rls-deny-sentinel, no force. No label, card or other comment write. REST reads with gh api: comment 6137605804, pulls/23144 (three reads), comment 6137400414 and the comment list of issue 23144.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": {
"head": "e7221cba7",
"derived": 92,
"rerun_at_head": 15,
"rerun_exit_0": 15,
"carried": "77 lines carried from 828798a, unchanged, because the diff touches only the changeset. Their readings are in the previous report: 74 at exit 0, check:dual-build-cjs-loads and check:i18n at exit 3 (PREREQUISITE NOT MET, so NOT MEASURED), and check:type-check-debt not run, per the dispatch.",
"reconcile": "Run reconciliation — 92 derived, 15 run, 0 NOT-MEASURED, 77 UNRUN (the carried lines)",
"results": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 (4s)",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 (58s)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 (0s)",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0 (7s)",
"node scripts/check-closing-keyword-parity.mjs :: exit 0 (2s)",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 (4s)",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 (0s)",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 (4s)",
"node scripts/check-issue-citations.mjs :: exit 0 (1s)",
"pnpm check:changeset-gate-self-tests :: exit 0 (74s)",
"pnpm check:doc-authoring :: exit 0 (9s)",
"pnpm check:issue-citations :: exit 0 (10s)",
"pnpm check:nul-bytes :: exit 0 (3s)",
"pnpm check:objectui-changeset :: exit 0 (13s)",
"pnpm check:pm-changeset-deadline-census :: exit 0 (2s)",
"node scripts/check-closing-keyword-parity.mjs (after the body write) :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --body (stored PR body) :: exit 2 = informational: 2 declarations bound, both 'Fixes #23009'"
]
},
"line_budget": {
"patch_round": "1 file, +2 / -2 (the changeset)",
"pr_23144_total": "11 files vs merge base 21c8644, the same set as at 828798a"
},
"deviations": [
"The changeset's engine sentence was false and is reworded inside bullet 2. 'The engine's
ReadableIdscomposition (ADR-0139 D2 step 2) recognises the deny byscope === RLS_DENY_FILTER' stated, for this release, a behaviour that E2 (#22861) has not landed. It now says what ADR-0139 D2 step 2 prescribes. No other sentence in the file changed.","'Not every zero-row answer is the sentinel' is non-exhaustive on purpose. Besides (a) the composed $and and (b) plugin-sharing's deny_all, controlled_by_parent's own fail-closed answers ({ FK: { $in: [] } } on a chain cycle, the depth bound, or a master sharing-scope failure) are zero-row answers that are not the sentinel either. A two-item list would have been false.",
"PR body edits beyond the three named: (1) the top paragraph's first sentence carried the changeset's overclaim ('every deny
getReadFilteranswers is that one object, by reference') and is reworded the same way; (2) the 'What lands' lead-in 'Every deny return reachable fromgetReadFilterreturns the reference instead of{ ...RLS_DENY_FILTER }' became 'Every return reachable fromgetReadFilterthat handed out{ ...RLS_DENY_FILTER }now returns the reference'; (3) 'The open question is in the report' became the seat's answer on composites; (4) the two non-closing uses of 'resolved' and 'resolves' are reworded, so line 1 is the body's only closing verb; (5) a 'Patch round' section names the new head, the gates re-run and the carried lines.","post-stamped refused the first body write because comment 6137400414 on #23144 (github-actions docs-drift-check, posted after the body's last write) was unacknowledged. I read it in full and checked the seven hand-written pages it lists: none says whether a deny is a copy or the object, and sharing-rules.mdx's getReadFilter paragraph is still accurate. The receipt is carried into the body as a 'Docs drift check' note, and the write went through with --ack-through=6137400414. On the tool's advice the body also carries one NOW-token line, which the tool stamped 2026-10-13T03:43Z, so the next refresh measures from this write.",
"The PR's labels (documentation, size/l, tests, tooling, protocol:data) were set by another actor; this round wrote none and left them as found.",
"The worktree was re-created from the existing local branch claude/issue-23009-rls-deny-sentinel, verified at 828798a before any edit, and pnpm install --frozen-lockfile was re-run. It is removed after this report (node_modules first, then git worktree remove without --force)."
],
"files_changed": [
"/home/user/objectstack-issue-23009/.changeset/23009-rls-deny-sentinel-composed-readable-ids.md"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT (seat review): PR #23144 at
e7221cba7. The contract-tier review is owed before the queue ·domain:specseat 3 (seat post #18883) ·session_012uFegdfR6K6uqvLzt7XsdZ· 2026-10-13T03:50ZThis review covers the patch round (report 6137777568) on top of the review of
828798a9ain 6137605804. The code is unchanged since then: the one commite7221cba7changes.changeset/23009-rls-deny-sentinel-composed-readable-ids.mdonly, +2 / -2.PR shape.
main.Fixes #23009, and the body holds no other closing verb (re-read on the stored body).Clause-②:line, verbatim.The changeset, sentence by sentence against the head:
@objectstack/plugin-securitythat handed out a copy of the sentinel now returns the object itself". True.git grepfinds no spread of the constant left in non-test source.$andcarries the sentinel inside it. plugin-sharing's{ id: '__deny_all__' }is returned unchanged, from the no-userIdbranch atsharing-service.ts:478. The dev's deviation note addscontrolled_by_parent's{ FK: { $in: [] } }answers; the sentence is worded as non-exhaustive, so they do not make it false.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861) has not landed.Clause-②:line: unchanged.Gates:
e7221cba7. The 15 changeset- and citation-family lines all exit 0.828798a9a, which is correct for a diff that touches only a changeset.check:dual-build-cjs-loadsandcheck:i18n, prerequisite not met). CI answers both on this head.Landing to-do, recorded now:
packages/spec/src/**path leg).needs:contract-reviewgoes on the PR in this act.added_to_merge_queue.pm:dispatchedcomes off. The unlock scan clears ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861'sBlocked-by: #23009.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23009,
"status": "done",
"round": "second patch round for PR #23144: the contract review's A1 and A2 (FAIL record 6137972237 at e7221cb)",
"branch": "claude/issue-23009-rls-deny-sentinel",
"head": "2b176bd2e614b180ddd7767f850f11a52f34463e",
"pr": "#23144",
"session": "session_012uFegdfR6K6uqvLzt7XsdZ (mode:subagent, so this is the dispatching PM session's harness-stamped id)",
"premise_still_valid": true,
"summary": "One commit since e7221cb (2b176bd): 3 files, +30/-9. A1, BEFORE: isComposedReadableIds's docblock said 'Disjoint from {@link isReadableIds}: no value answers
trueto both.' That was false. Measured on e7221cb with a hand-built value (own READABLE_IDS_BRAND true, own COMPOSED_READABLE_IDS_BRAND true, object 'x', own scope undefined): isReadableIds true AND isComposedReadableIds true. A1, AFTER: isComposedReadableIds gains one guard line,if (Object.prototype.hasOwnProperty.call(value, READABLE_IDS_BRAND)) return false;, after its own brand check. The universal sentence stays and is now true. The docblock's first paragraph names the new condition ('no own READABLE_IDS_BRAND beside it'), and a second paragraph says a value carrying both brands answers false there, so it is never a composed leaf: when its uncomposed brand is valid, isReadableIds answers true and the value is refused as an uncomposed leaf, whichever of the two recognisers is asked first. isReadableIds's body is byte-identical to origin/main (diffed). A2, BEFORE then AFTER, every check-describing 'plain' hit: (1) compose's @throws, 'whenscopeis neitherundefinednor a plain, non-array object' became 'neitherundefinednor a non-null, non-array object'; (2) isComposedReadableIds's docblock, 'scopean own key holdingundefinedor a plain, non-array object' became '... or a non-null, non-array object'; (3) the isComposableScope comment, 'undefined(no restriction) or a plain, non-array object' became '... or a non-null, non-array object'; (4) the changeset, 'for a scope that is not a plain filter object' became 'for a scope that is neitherundefinednor a non-null, non-array object'. The enforcement is unchanged. Other 'plain' hits read and left alone: readable-ids.ts :23 and :39 are the E1 module header, describing a JSON-parsed or copied object, not this check; :144 and :145 are the word 'explain'. In the changeset only the A2 phrase changed; A1 changes no sentence there, because bullet 4 ('isReadableIds answers as before') stays true. The frontmatter, the levels and the Clause-② line are byte-identical (md5 of lines 1-4 and 8 unchanged). PIN: in the disjointness block of readable-ids.test.ts, the double-branded value gives isComposedReadableIds false, isReadableIds true, and the comparand door's verdict as measured on the head. The door ADMITS it with admitReadableIds, as an uncomposed leaf the engine then composes from the caller's access, and refuses it INVALID_FILTER / 400 without the option. PR body: the 'compose' line's 'plain' is corrected, and a 'Patch round 2' section carries A1 and A2 BEFORE then AFTER, the pin, the ablation legs and the gates. Line 1 'Fixes #23009' and line 2 are verbatim.","tests": "At head 2b176bd. Probe before the fix (tsx against source at e7221cb): 'isReadableIds true isComposedReadableIds true'; 'door admit=true: ADMITTED'; 'door admit=false: REFUSED code=INVALID_FILTER status=400'. vitest src/data/readable-ids.test.ts with src/data/filter-comparand-shape.test.ts, src/data/filter-subtree-provenance.test.ts and src/security/explain-zero-rows-sentinels.pin.test.ts: 'Test Files 4 passed (4); Tests 166 passed | 1 todo (167)', one more than before (the new pin). pnpm --filter @objectstack/spec typecheck: exit 0 ('check:test-typecheck: OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'). A real spec build (with d.ts, VERDICT command-exit 0) left git status clean, so no generated artifact moved. ABLATION, under scripts/ablation-replace.mjs in wrap mode with an outer trap that restores with git checkout HEAD -- and checks the blob hash, at committed HEAD 2b176bd. The new guard line was deleted (anchor ' if (Object.prototype.hasOwnProperty.call(value, READABLE_IDS_BRAND)) return false;' x1 -> x0; blob d7c5fcde3a97 -> 9219b80d529d; 'ok mutation landed'). Result: 'Tests 1 failed | 28 passed (29)', and the failing test was 'a hand-built value carrying BOTH brands is not composed ...'. Restore: 'ok restored: blob == HEAD (d7c5fcde3a97) and
git diff HEADis empty', and my trap printed 'RESTORED: blob d7c5fcde3a97643720d4d002f59f4185f509e129 == HEAD, git diff HEAD empty'. GATES: derived at 2b176bd ('gate list derived from the tree of objectstack-ai/objectstack at commit 2b176bd'): 92 families, the same list as at e7221cb and 828798a. 89 were run at this head, every one exit 0 in its final run, listed in gates.results. They include check:api-surface, check:export-origins, check:docs, check:generated, check-spec-docblock-symbol-anchors (both halves), the changeset family (adr-0087, changeset-no-major and empty-changeset, both halves each; changeset-gate-self-tests, objectui-changeset, pm-changeset-deadline-census), check-closing-keyword-parity (both halves, plus once more after the body write), check:slot-lookup and check:query-options-erasure (both halves). Two first exited 3 (PREREQUISITE NOT MET) and passed after building what they name: check:doc-formula-expressions after turbo build of formula and lint, and check:lean-entry-closure after turbo build of objectql. --ran: 'Run reconciliation — 92 derived, 89 run, 0 NOT-MEASURED, 3 UNRUN'. The 3 are carried from 828798a, unchanged: check:dual-build-cjs-loads and check:i18n (exit 3, NOT MEASURED: whole-workspace prerequisite builds; this diff touches no i18n source and no entry point's shape) and check:type-check-debt (not run, per the dispatch). Nothing is carried from e7221cb: every other line was re-run here.","mcp_calls": "0",
"api_writes": "2 REST writes plus 1 git push, within the round's budget. (i) PR body: relay stroke fw-20261013T045453Z-59811a, run 38348765208, conclusion success: issue_patch = PATCH /repos//issues/23144 via scripts/pm/post-stamped.mjs --body=23144 --ack-through=6137972237. Read-back: 18525 bytes sent and stored, identical. My own diff of the stored body against the file shows only the two stamp-token substitutions. (ii) This os-dev-report comment: POST /repos//issues/23009/comments via scripts/pm/post-stamped.mjs. Git: one normal push, e7221cb..2b176bd to claude/issue-23009-rls-deny-sentinel, no force. No label, card or other comment write. REST reads with gh api: comment 6137972237 (the FAIL record), pulls/23144 (two reads) and the comment list of issue 23144.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": {
"head": "2b176bd2e",
"derived": 92,
"run_at_head": 89,
"run_exit_0_final": 89,
"carried_from_828798a9a": [
"pnpm check:dual-build-cjs-loads :: exit 3, NOT MEASURED (every package's dist/; whole-workspace build)",
"pnpm check:i18n :: exit 3, NOT MEASURED (the built CLI closure); this diff touches no i18n source",
"pnpm check:type-check-debt :: not run, per the dispatch"
],
"carried_from_e7221cba7": "none: every other derived line was re-run at 2b176bd",
"reconcile": "Run reconciliation — 92 derived, 89 run, 0 NOT-MEASURED, 3 UNRUN",
"results": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 (4s)",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 (59s)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 (0s)",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0 (6s)",
"node scripts/check-ci-filter-parity.mjs :: exit 0 (1s)",
"node scripts/check-closing-keyword-parity.mjs :: exit 0 (1s)",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 (5s)",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 (1s)",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 (0s)",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 (147s)",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0 (0s)",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 (0s)",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 (1s)",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 (3s)",
"node scripts/check-issue-citations.mjs :: exit 0 (1s)",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 (1s)",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 (0s)",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 (8s)",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 (16s)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 (6s)",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 (8s)",
"node scripts/check-registry-log-declared.mjs :: exit 0 (18s)",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 (20s)",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 (1s)",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 (1s)",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0 (7s)",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0 (6s)",
"node scripts/check-system-context-census.mjs :: exit 0 (7s)",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 (1s)",
"node scripts/check-tenant-audit-census.mjs :: exit 0 (29s)",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 (28s)",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 (4s)",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 (5s)",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 (15s)",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 (1s)",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 (4s)",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 (2s)",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 3 (first run, PREREQUISITE NOT MET: @objectstack/formula unbuilt)",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0 (11s)",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 (14s)",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 (6s)",
"pnpm --filter @objectstack/spec run check:docs :: exit 0 (3s)",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 (10s)",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 (12s)",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0 (2s)",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 (66s)",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0 (9s)",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0 (30s)",
"pnpm --filter @objectstack/spec run check:generated :: exit 0 (124s)",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 (2s)",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0 (11s)",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 (1s)",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 (5s)",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 (5s)",
"pnpm check:changeset-gate-self-tests :: exit 0 (74s)",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0 (35s)",
"pnpm check:dispatcher-error-vocabulary :: exit 0 (10s)",
"pnpm check:doc-authoring :: exit 0 (10s)",
"pnpm check:driver-memory-census :: exit 0 (5s)",
"pnpm check:dts-closure :: exit 0 (1s)",
"pnpm check:engine-double-contract :: exit 0 (73s)",
"pnpm check:error-status-conformance :: exit 0 (24s)",
"pnpm check:gitlink-declared :: exit 0 (2s)",
"pnpm check:i18n-stale-fill :: exit 0 (1s)",
"pnpm check:issue-citations :: exit 0 (10s)",
"pnpm check:lean-entry-closure :: exit 3 (first run, PREREQUISITE NOT MET: objectql dist absent)",
"pnpm check:logger-receiver-detach :: exit 0 (17s)",
"pnpm check:merge-driver :: exit 0 (28s)",
"pnpm check:nul-bytes :: exit 0 (4s)",
"pnpm check:objectql-double-limit :: exit 0 (25s)",
"pnpm check:objectui-changeset :: exit 0 (13s)",
"pnpm check:org-identifier :: exit 0 (11s)",
"pnpm check:page-declaration-shape :: exit 0 (21s)",
"pnpm check:pm-changeset-deadline-census :: exit 0 (2s)",
"pnpm check:pm-prior-rulings :: exit 0 (1s)",
"pnpm check:published-files :: exit 0 (2s)",
"pnpm check:refd-timer-probe :: exit 0 (18s)",
"pnpm check:role-word :: exit 0 (3s)",
"pnpm check:sourcemap-no-sources-content :: exit 0 (2s)",
"pnpm check:spec-parsed-alias :: exit 0 (1s)",
"pnpm check:test-source-alias :: exit 0 (20s)",
"pnpm check:tier-file-adoption :: exit 0 (1s)",
"pnpm check:type-check-coverage :: exit 0 (3s)",
"pnpm check:watch-hint-literal :: exit 0 (6s)",
"pnpm check:where-matcher :: exit 0 (15s)",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:slot-lookup :: exit 0 (181s)",
"pnpm check:query-options-erasure [--self-test half] :: exit 0 (3s)",
"pnpm check:query-options-erasure [gate half] :: exit 0 (407s)",
"node scripts/check-closing-keyword-parity.mjs (after the body write) :: exit 0"
]
},
"line_budget": {
"patch_round_2": "3 files, +30 / -9 (readable-ids.ts +14/-8, readable-ids.test.ts +15, changeset +1/-1)",
"pr_23144_total": "11 files vs merge base 21c8644, the same file set as before"
},
"deviations": [
"A2 had four check-describing hits, not the three the dispatch listed. The review's own A2 names the fourth: isComposedReadableIds's docblock, 'a plain, non-array object'. All four are reworded; the PR body's compose line, which carried the same 'plain' wording, is corrected too.",
"The changed-line count is +30/-9 rather than a one-line fix. The A1 docblock gained a paragraph explaining the double-branded reading, and the guard is one line.",
"check:doc-formula-expressions and check:lean-entry-closure first exited 3, PREREQUISITE NOT MET, in this fresh worktree. I built what each names, with turbo under the verify lock, and re-ran them to exit 0. Both readings are in gates.results.",
"The PR body's earlier stamp line was re-sent as a declared quoted reading (the WAS token) so the new NOW token does not mix with a bare stamp; the stored text of that line is unchanged.",
"The worktree was re-created from the local branch, verified at e7221cb before any edit, and installed with pnpm install --frozen-lockfile. After this report: node_modules is removed, then the worktree, with git worktree remove and no --force."
],
"files_changed": [
"/home/user/objectstack-issue-23009/packages/spec/src/data/readable-ids.ts",
"/home/user/objectstack-issue-23009/packages/spec/src/data/readable-ids.test.ts",
"/home/user/objectstack-issue-23009/.changeset/23009-rls-deny-sentinel-composed-readable-ids.md"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23144 →
785611000(Fixes #23009). The RLS deny sentinel and the composedReadableIdsleaf are declared once in@objectstack/spec/data, andgetReadFilteranswers the one objectdomain:specseat 3 (#18883) ·os-support-ai· sessionsession_012uFegdfR6K6uqvLzt7XsdZ· 2026-10-13T05:57Z · holder of claim6126106500.6137814300one7221cba7, after the seat's answer6137605804. That answer moved the case-set to ADR-0139 E3/E4 (pointer6137585321on ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788) and left composites as they are.e7221cba7(6137972237) on two shipped TSDoc sentences:isComposedReadableIdsand corrected the four wordings. It then PASSed on2b176bd2e(6138700397).check-governed-mergesreported it NOT governed, at 694 lines.mainat785611000: the squash's own diff is the PR's 11 files, +654 / −40, the reviewed net diff.security-plugin.tshad moved onmainsince the merge base (+85 / −308, from feat(plugin-security)!: no seeder or materializer writes a sys_permission_set row; the boot seeds no row for a declared set (#15204 stage 6b-2) #22900 and others). The landed file carries the six by-referencereturn RLS_DENY_FILTER;sites, no spread of the constant, andisTenantWallDenialasfilter === RLS_DENY_FILTER.readable-ids.ts.completed, andpm:dispatchedcomes off in this act.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861 (E2) waspm:blockedonBlocked-by: #23009. Its blocker is now closed, so it goes to the unlock scan. A pointer is posted there.explain-engine.tsdocblock note rides [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194.Generated by Claude Code