Repository navigation
feat(security)!: resolve current_user.queues kernel-side and reserve the queues membership key - #27044
Conversation
…e key Claude-Session: https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA Co-Authored-By: Claude <noreply@anthropic.com>
…he skill line's tokens Claude-Session: https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 33 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 150 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7f01128247a8fd1b5e35b52c183005b10c425e63 && git checkout 7f01128247a8fd1b5e35b52c183005b10c425e63
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3230bdbf76844966623e21db5ac940e4b83fd1b9 e37497434c3983744185c320e40e5f698d1e3251 && git checkout -B drift-repro 3230bdbf76844966623e21db5ac940e4b83fd1b9 && git merge --no-ff e37497434c3983744185c320e40e5f698d1e3251
node scripts/docs-audit/affected-docs.mjs --json 3230bdbf76844966623e21db5ac940e4b83fd1b9
|
Contract reviewServed-tier: Inputs read: card #27016 (body, claim 6458269562, amendment 6458307227, os-dev-report 6459486195), ruling 6457475657 and brief 6455628190 on #25769, PR #27044 body and file list (21 paths, equal to Check-runs on the head at the time of this record: 47 total, 27 success, 8 skipped, 0 failure, 12 in progress — ① Derived judgments
② Semver levelChangeset Clause-② read against the tree: the narrowing is real (the refine refuses the name) and the widening half is the platform's own value where before there was none, which the changeset states. Correct. ③ Boundary flags
④ Design judgmentThe narrowing, read against the published spec on Tried to break it:
Findings:
Implemented-by: VERDICT: PASS |
…security.md token ceiling Claude-Session: https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA Co-Authored-By: Claude <noreply@anthropic.com>
|
Not this PR's:
The new head Generated by Claude Code |
席位核对 · ACCEPT ·
|
|
Contract-review-claim: Triage seat, under the maintainer-ruled gap rule (#27062, 「可以,就按这个执行」), condition (b). The record on file (6459684406, PASS) names Generated by Claude Code |
Contract reviewServed-tier: Delta review on a moved head. The record on file (6459684406, PASS, Reviewed-by the lane's session) names Inputs read: card #27016 (body; claim 6458269562; amendment 6458307227; os-dev-report 6459486195), ruling 6457475657 and brief 6455628190 on #25769 (fetched by Check-runs on ① Derived judgmentsThe two delta files publish nothing and move no accept set. Each is judged in its own right, then the earlier record's ten judgments are re-read against the unchanged tree.
② Semver levelUnchanged since ③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by Yibo Wang · project thread
Fixes #27016
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs:
skills/objectstack-data/rules/security.mdlists the kernel names a membership key may not take;queuesis added there (paid for by a deletion in the same file; the ratchet row is lowered 3693 → 3687 to sit at the measurement). Thecurrent_user.*placeholder table and the queue authoring guide are #26475's.维护者速读
queues加入RESERVED_RLS_MEMBERSHIP_KEYS;plugin-security从 ownership-queue 服务的queuesOf解析current_user.queues(与认领闸门isMember同一份成员答案),每个请求最多一次,且只在即将编译的策略读到这个键时才解析。6457475657(甲)——一个内核侧生产者;应用 resolver 提供的queues被筛掉并失败即关闭。membershipKeys再也不能声明名为queues的键,resolver 的queues答案被丢弃并 WARN。仓内声明/resolver 普查为 0。回滚 = revert 本 PR。packages/plugins/plugin-security/src/kernel-queue-membership.ts(新生产者)、rls-compiler.ts的userContextOf一行、security-plugin-rls-filter.ts的stageRlsMembership、.changeset/27016-queues-reserved-membership-key.md。pnpm --filter @objectstack/plugin-security exec vitest run src/kernel-queue-membership.test.ts src/rls-reserved-membership-keys.test.ts,预期全绿。What changes
packages/spec/src/contracts/rls-membership-resolver.ts):queuesjoinsRESERVED_RLS_MEMBERSHIP_KEYS, with a doc comment saying what it holds, who resolves it and why an app may not. The module header's "What does NOT belong here" and thekeysdoc name it too.rls.zod.ts(the "Where a membership key comes from" docblock) and theObjectSchema.membershipKeys.describe()list it among the kernel names; the reference pages are regenerated (gen:docs, 4 files).kernel-queue-membership.ts:stageKernelQueuesresolvesqueuesOf(userId, tenantId)once per request and stores it on the context under a module-private symbol, with the principal and organization it was resolved for;kernelQueuesOfanswers only for that identity. So a caller cannot forge it (context.queuesis never read), and a context copied for another user or organization does not carry it.stageRlsMembershipstages it first, and only when a policy about to compile readscurrent_user.queues(policiesReadCurrentUserKey, a sibling ofmembershipKeyReadsOfon the same parsed-once CEL walk inobject-membership-keys.ts). Both compile sites (readusing, writecheck) already call it with their policies.RLSCompiler.userContextOfsetsqueues: kernelQueuesOf(executionContext)besidepositions, andRLSUserContextdeclares it.SecurityPlugin.ownershipQueueMembership()resolves the sameownership-queueservice asownershipQueues()(the claim gate's), forqueuesOf.stageResolverMembershipand the compiler merge both refuse a reserved key by name, so withqueueson the list an app resolver's or a bag'squeuesis dropped and the policy reads the kernel value or fails closed.KERNEL_KEY_RUNTIME_TYPEgainsqueues: 'array'(the probe already binds a key with noExecutionContextfield as an array). The threeos explaintexts that list the kernel keys addqueues; the LISTS sentence no longer saysExecutionContextdeclares every list key, sincequeueshas noExecutionContextfield. The unknown-variablefix:line says "Read a kernel key (…)" instead of "Read a pre-resolved key (…)": the longer key list put it at 201 characters against its 200 bound.skills/objectstack-data/rules/security.md.ADR-0142 D3 amended in place. The D3 sentence that named the queue service as the registered
IRlsMembershipResolverforqueues(option B's mechanism) now says what the tree does:queuesis a reserved key that plugin-security resolves kernel-side from the queue service, the way it resolvespositions, and an app resolver answeringqueuesis screened out and fails closed. It carries an inline Amended note citing ruling 6457475657 (#25769, letter A). The Execution plan bullet about the PR that already landed is left as history.Why
Ruling
6457475657on #25769 (letter A):queuesis reserved and resolved kernel-side in plugin-security, one producer, likepositions; an app resolver that supplies it is screened and fails closed. Options B (plugin-sharing's own resolver) and C (drop the key) were not chosen.On the declaration: the card said
yes (widening)/none. Re-measured,RESERVED_RLS_MEMBERSHIP_KEYSis also read byMembershipKeyNameSchema(rls.zod.ts, the refine onObjectSchema.membershipKeyskey names), so the change refuses an authoredmembershipKeys.queuesat parse. Seat 1 ruled itno (narrowing)/narrowed, with ADR-0087not-required (no-migration-prescription)and a**The remedy.**line, since the new name is the author's to choose.Readers and producers (measured on
origin/main7eb613c)RESERVED_RLS_MEMBERSHIP_KEYSreaders:MembershipKeyNameSchema(spec),stageResolverMembershipandRLSCompiler.userContextOf(plugin-security),membershipKeyReadsOf(plugin-security, excludes reserved names from declared-key staging), lint'sPRERESOLVED_USER_KEYS. Every one reads the list by import, so no copy needed editing.object-membership-keys.test.ts(it.each over the list, no edit), plugin-securityrls-reserved-membership-keys.test.ts(CELLSmust equal the list:queuescell added), lintKERNEL_KEY_RUNTIME_TYPEand the explanation-text pins.membershipKeysdeclarations namedqueuesand zero resolvers declaringqueuesinpackages,examples,qa,skillsorcontent.queuesOfvsisMemberinplugin-sharing'sOwnershipQueueService: both answer throughspecsReachUserwith theaccesspurpose (switched-off positions confer nothing on either). A new test there pins that they agree for every user, queue and organization of the fixture.SharingService.callerQueues) also readsqueuesOf; it is the owner match's disjunct, not the RLS key, and is unchanged.Tests
plugin-security/src/kernel-queue-membership.test.ts(12 cases, through the whole plugin'sgetReadFilter): a member reads{ queue: { $in: [their queues] } }in the active organization, and another organization's set in another; a non-member gets the deny sentinel, byte-identical to no queue service; the set agrees withisMember; resolved once per request and not at all when no policy reads the key; no user or a throwing service fails closed (one WARN); an app resolver answeringqueuesis dropped with the reserved-key WARN and the kernel set stands, a non-member it tried to seat reads nothing, and with no queue service it does not fill the gap; aqueuesfield or bag entry on the context is never read; a copied context for another user or organization does not carry the set.plugin-security/src/rls-reserved-membership-keys.test.ts: thequeuescell (bag refused byte-identically onusingandcheck, kernel value wins).plugin-sharing/src/ownership-queue.test.ts:queuesOfagrees withisMember(both answers occur, so not vacuous).owner_id == null && queue in current_user.queuescompiles to{"$and":[{"owner_id":{"$null":true}},{"queue":{"$in":[…]}}]}.Ablation (on committed heads, restored with
git checkout HEAD): compiler line set toqueues: undefinedgives 5 failed (member, organization, isMember agreement, resolver screened,queueskernel-wins cell);queuesremoved from the built spec list gives 4 failed (is reserved, resolver screened, no-service gap, caller-forged field).Verification
Runs on head
8660c53ef(worktree offorigin/main7eb613c3):@objectstack/spec738 files / 22131 passed (1 todo);@objectstack/plugin-sharing57 files / 1279 passed;@objectstack/lint170 files / 7510 passed;@objectstack/plugin-security234 files / 5065 passed, 45 skipped;typecheckfor all four packages exit 0. (The first lint run found two pins this diff moves: the unknown-variablefix:line at 201 characters, and the engine-judge test's copy of the explanation's key list. Both fixed in8660c53ef, then lint and plugin-security re-ran green.)pnpm --filter @objectstack/spec check:generated: onlycontent/docs/references/**was stale; regenerated withgen:docsand committed.dispatch-gates --commands: 115 derived commands. 111 exit 0 on the first sweep.check-skills-token-ratchetwas red (+3 tokens onsecurity.md), paid by a deletion in the same file, then exit 0.check:skill-examples,check:dual-build-cjs-loadsandcheck:i18nexited 3 (built output missing) and exit 0 after the dogfood closure build.@objectstack/dogfood^...closure build plus the whole suite under the lock: 344 files passed, 3 skipped, 1 failed (348); 2788 tests passed, 20 skipped, 2 failed. Both failures arepackaged-action-disabled-dispatch-live.dogfood.test.ts(acceptance[0] and [5]/[2]), and they reproduce when that file runs alone. Not this diff: the test reads the firstshowcase_taskunsorted ($top=1) and expects it not yet done, and here that row was alreadydone: truebefore the action ran (beforeequalsafter). Nothing in the showcase or dogfood stacks readscurrent_user.queues(git grep), so the new staging never runs there. Reported to the seat as an out-of-scope finding.Human Minimum
Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.
packages/specholds no runtime logic.pnpm lintpasses. (No formatter ran.pnpm lintis CI's repo-wide run.)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed (gen:docs;check:generatedotherwise up to date).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzhD6Fqfyavauv2KoGA