Uh oh!
There was an error while loading. Please reload this page.
test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220
Conversation
…grant route — allowTransfer (#14137) The independent-property floor (#14029) read the three CRUD write flags and modifyAllRecords, missing the evaluator's FIRST grant route: the direct bit read off OPERATION_TO_PERMISSION (transfer: 'allowTransfer'), ENFORCED today through the insert/update owner_id door (#3004). A transfer-only wildcard default set would never have been required to be a managed-deny target. Extend the floor with a value test (=== true, never key-existence — the bits are present-as-false via .default(false), #14129 first review), sync both exhaustive docblocks to name the route, and pin: positive transfer-only shape, read-only reverse control, dedicated present-as-false pin, and a zero-delta invariance sweep over the seeded sets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
…naged-deny-floor-allowtransfer
…naged-deny-floor-allowtransfer
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 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 76d1732babf1197a6ba3799e2d18abf1a18b5c58 && git checkout 76d1732babf1197a6ba3799e2d18abf1a18b5c58
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1134e47a0d5e769bf518ca45e0c0b14e017ca19b 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 && git checkout -B drift-repro 1134e47a0d5e769bf518ca45e0c0b14e017ca19b && git merge --no-ff 6fec5c3e8e0104c6acb08e57109f91643a70a7b0
node scripts/docs-audit/affected-docs.mjs --json 1134e47a0d5e769bf518ca45e0c0b14e017ca19b
|
os-steve
commented
Sep 1, 2026
PM 复核 — ACCEPT。⛔ 无需契约复审, |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14137
The managed-deny floor now sees the permission evaluator's first grant route.
What changed (3 files, +141 / −22 vs merge-base
b992b1d64)packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts— the independent-property floor introduced by organization_admin_no_bypass holds a write-granting wildcard but is not in MANAGED_DENY_TARGET_SETS, so registry-derived managed-object denies never reach it #14029 gains a fifth clause,wc.allowTransfer === true, and four new pins judge it. The filter is factored intograntsWildcardWrite+deriveWriteGrantingWildcardSetsso the same derivation that runs against the seededdefaultPermissionSetscan also be pointed at synthetic wildcards — the shapes no seeded set carries yet.packages/plugins/plugin-security/src/managed-object-write-denies.ts— docblock only. The exhaustive claim onMANAGED_DENY_TARGET_SETSnow namesallowTransferalongside the three CRUD flags andmodifyAllRecords, and states what the clause list does not claim (no census of grant routes beyond the evaluator's two known tables — The managed-deny floor still misses anallowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape #14137 "Not established"). Keeping an exhaustive claim in step with a narrower implementation is precisely what produced fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129's FAIL; the pairing is the point..changeset/managed-deny-floor-allowtransfer.md— patch.The clause is a value test (
=== true), never key-existence: Zod materialises these bits with.default(false)(permission.zod.ts), so they are present-as-false and an existence test misfires today. Established in #14129's first review; not re-litigated here. A dedicated pin holds that line red-if-rewritten.Not done here, deliberately: the card's suggestion 3 (derive the floor from
OPERATION_TO_PERMISSION/MODIFY_ALL_WRITE_KEYSinstead of restating bits by hand) is unauthorised on this card — its honest prerequisite, a census of the evaluator's grant routes, has not been done.Premise re-verified against the tree at
b992b1d64(not copied from the card — line numbers moved when #14129 landed)OPERATION_TO_PERMISSIONstill maps transfer, and the table is LIVE.packages/plugins/plugin-security/src/permission-evaluator.ts:31→transfer: 'allowTransfer',⭐ Reverse control, same grep shape (
^\s+(transfer|update):\s*'allow): it also returns:29 update: 'allowEdit',— so the shape is not tuned to find only what I wanted. Liveness is separately measured: three call sites read the table —:64(const key = OPERATION_TO_PERMISSION[op]),:112(switch (OPERATION_TO_PERMISSION[operation])),:208(const permKey = OPERATION_TO_PERMISSION[operation]). Not dead code.allowTransferis still.default(false)and still ENFORCED.packages/spec/src/security/permission.zod.ts:193→allowTransfer: z.boolean().default(false).describe('[RBAC-gated; ENFORCED via the insert/update owner_id guard] ...'), with the 安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004 docblock above it spelling out the insert/updateowner_iddoor. Read only —packages/spec/**is not touched by this PR.grantsWildcardWriteatdefault-permission-sets.test.ts:405;MANAGED_DENY_TARGET_SETSatmanaged-object-write-denies.ts:117.Ablation — performed on this branch at
6fec5c3e8, not cited from anyoneMutation: delete
|| wc.allowTransfer === truefromgrantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so nodistleg is involved for it; the imports it reads are untouched by the mutation.Proven on disk before measuring — never from an editor exit code:
Result — exactly one test flips, and it is the card's positive pin:
The three controls stayed green under the mutation — a declared control that goes red both ways is not evidence, and neither is one that is green both ways for the wrong reason. Unmutated baseline on the same file, same run shape:
Tests 26 passed (26).Restore proven by state, not by exit code:
The script carried
trap restore EXIT INT TERMwith an absoluteREPO_ROOT-anchored path throughout.Verification bar from the card — all four pins present
a transfer-only wildcard is required to be a managed-deny target (the card)— asserts afterPermissionSetSchema.parsethat all three CRUD flags andmodifyAllRecordsreally are (present-as-)false, then that the derivation returns the setreverse control: a read-only wildcard is still NOT requiredallowTransfer: falsemust NOT trippresent-as-false: an explicit allowTransfer:false wildcard does not trip the floor— also asserts'allowTransfer' in wc === true, so a key-existence rewrite of the floor turns exactly this pin redinvariance: the allowTransfer clause changes no existing seeded set verdict— diffs the new floor against the restated pre-#14137 floor for every seeded wildcard, with a non-vacuousness assertion that the loop really visited themGate readings — derived live at
6fec5c3e8, exits captured BEFORE any pipenode scripts/pm/dispatch-gates.mjs(no paths; it takes its own change set from the merge base) named 36 commands — 30 by path, 8 by change kind. All 36 run below.The three the previous agent never re-read after the merge — re-run at the post-merge head, quoting each gate's own verdict line:
pnpm check:type-source-resolution— EXIT=0 —check-type-source-resolution OK — 96 tsc program(s) across 77 packages scanned ...; 54 registered as still resolving a workspace dep's types through dist/.pnpm check:where-matcher— EXIT=0 —✓ where-matcher conformance holds: 323 matcher(s) discovered, 323 answer the combinator battery correctly or refuse it loudly (204 refuse). 0 silently-wrong and 0 unjudged matcher(s) ...; none new. baseline key set verified against b992b1d: no files added.pnpm check:type-check-debt— EXIT=0 (captured before any pipe) —✓ check:type-check-coverage --self-test — 47 semantic case(s) + 65 observation case(s) + 43 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) + 18 exit-code case(s) hold.thencheck-type-check-coverage --re-measure: OK — 27 ledger entr(ies) re-measured in 362.2s, 1217 raw tsc error(s) total, none above its recorded number.·surplus: none — every entry sits exactly at its measurement, so any new error is red.The workspace closure was built first, exactly aslint.ymldoes, so this is a measurement and not the gate refusing.Remaining 33, each
EXIT=0unless noted:check:changeset-gate-self-tests·check:cross-package-test-inputs·check:doc-authoring·check:dual-build-cjs-loads·check:engine-double-contract·check:i18n·check:i18n-stale-fill·check:logger-receiver-detach·check:objectql-double-limit·check:objectui-changeset·check:page-declaration-shape·check:pm-half-states·check:published-files·check:query-options-erasure·check:slot-lookup·check:test-source-alias·check:type-check-coverage·scripts/check-adr-0087-registration.mjs·scripts/check-changeset-no-major.mjs·scripts/check-ci-filter-parity.mjs·scripts/check-comment-mask-adoption.mjs·scripts/check-cross-package-test-inputs.mjs·scripts/check-empty-changeset.mjs·scripts/check-keyed-text-bounds.mjs·scripts/check-plugin-teardown-shape.mjs·scripts/check-shard-attestation.mjs·scripts/check-tenant-audit-census.mjs·scripts/check-undeclared-dep-imports.mjs·scripts/docs-audit/check-affected-docs.mjs·scripts/docs-audit/check-drift-comment.mjs·scripts/pm/check-half-states.mjs·scripts/pm/release-rehearsal-clone.mjs --self-testnode scripts/check-test-completeness.mjs— EXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a savedturbo run testlog and the family names it with no argument, so there is nothing local to hand it. CI tees the log and passes the path, so CI behaviour is unchanged.check-system-context-censusis not in the derived family for this diff, and this PR touches nocontent/docs/**path. Nothing was hand-edited there and no--fixwas needed or run.Package verification (
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'first, VERDICT command-exit 0):pnpm --filter @objectstack/plugin-security typecheck(three programs:tsconfig.json,tsconfig.scripts.json,tsconfig.test.json) — EXIT=0.⭐ Not "ran but measured nothing":
tsc --listFilesconfirms both edited files really sit in a program that ran —default-permission-sets.test.tsis in thetsconfig.test.jsonprogram, andmanaged-object-write-denies.tsis in both the main and the test program.pnpm --filter @objectstack/plugin-security exec vitest run— EXIT=0,Test Files 94 passed (94) · Tests 1774 passed (1774).Repo-wide scans (
pnpm lintand the rest of the always-runs tail) are CI's run, not owed here.Clause-② declaration: no — both limbs answered separately
1. Path limb — no. The diff touches
packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts,packages/plugins/plugin-security/src/managed-object-write-denies.ts, and one changeset. Nothing underpackages/spec/src/**—permission.zod.tswas read only, to confirm.default(false)and ENFORCED.2. Content limb — no, argued on both halves, and measured rather than assumed.
Does it change accept/reject behaviour? The judging criterion does change, and that is not the same statement as "zero behaviour delta today". The independent-property floor now judges a transfer-only wildcard (
allowRead+allowTransfer, three write flags andmodifyAllRecordsall false) as required-to-be-a-managed-deny-target, where before it passed silently — a genuine tightening. But that criterion is a repo-internal vitest pin gating this repo's own seeded defaults at CI time. No runtime accept/reject moves:applyManagedWriteDenies,MANAGED_DENY_TARGET_SETS,MANAGED_DENY_ENTRY, the permission evaluator and every Zod schema are unchanged, so no metadata a user publishes is accepted or rejected differently. The invariance pin additionally holds every existing seeded set to its exact prior verdict.Does it add or widen ANY export?No — measured, not eyeballed. Three readings, each falsifiable:
git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'returns nothing: zero export statements added, removed or changed.managed-object-write-denies.tscarries a*docblock prefix —grep -vE '^[+-] \* 'over its changed lines returns nothing, so that file's delta is comment-only and its emitted JavaScript is unchanged.plugin-security'sexportsmap has one entry (.→dist/index.*),managed-object-write-denies.tsis not re-exported fromsrc/index.ts(onlysecurity-plugin.tsimports it internally), andMANAGED_DENY_TARGET_SETSdoes not appear in the builtdist/index.d.ts. The third file is a.test.ts, whichfiles: ["dist", "README.md", "CHANGELOG.md"]never ships.⇒ no
needs:contract-reviewlabel is owed on the card or this PR.Landing
Draft on purpose. ⛔ Not flipped to ready, ⛔ no auto-merge — landing is the PM's.
Every reading above was taken at
git rev-parse --short HEAD=6fec5c3e8, which is the final commit on this branch — the ablation restored the tree by state before the gate union ran, and no commit landed after it, so nothing here reports the green of a tree that is no longer head.Generated by Claude Code
Generated by Claude Code