test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer - #14220

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer
Sep 1, 2026
Merged

test(plugin-security): the managed-deny floor sees the evaluator's first grant route — allowTransfer#14220
os-steve merged 3 commits into
mainfrom
claude/issue-14137-managed-deny-floor-allowtransfer

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

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)

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_KEYS instead 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)

  1. OPERATION_TO_PERMISSION still maps transfer, and the table is LIVE.
    packages/plugins/plugin-security/src/permission-evaluator.ts:31transfer: '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.
  2. allowTransfer is still .default(false) and still ENFORCED.
    packages/spec/src/security/permission.zod.ts:193allowTransfer: 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/update owner_id door. Read only — packages/spec/** is not touched by this PR.
  3. The floor and the list are where the change puts them.grantsWildcardWrite at default-permission-sets.test.ts:405; MANAGED_DENY_TARGET_SETS at managed-object-write-denies.ts:117.

Ablation — performed on this branch at 6fec5c3e8, not cited from anyone

Mutation: delete || wc.allowTransfer === true from grantsWildcardWrite, restoring the pre-#14137 four-clause floor. The mutated symbol lives inside the test file, so no dist leg is involved for it; the imports it reads are untouched by the mutation.

Proven on disk before measuring — never from an editor exit code:

HEAD blob for .../default-permission-sets.test.ts = bf48dfc975d1a102c29a85e352cbff6c8e472355
pre-mutation on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355
MUTATION PROOF: deleted-text 'wc.allowTransfer === true' count = 0 (expect 0, was 1)
MUTATION PROOF: injected-text 'wc.modifyAllRecords === true;' count = 2 (expect 2, was 1)
MUTATION PROOF: on-disk hash = dacc387b1933bacce39f809a7bbc1b4906815cc5 (differs from the HEAD blob)

Result — exactly one test flips, and it is the card's positive pin:

 ❯ src/objects/default-permission-sets.test.ts (26 tests | 1 failed) 58ms
× a transfer-only wildcard is required to be a managed-deny target (the card) 14ms
Test Files 1 failed (1)
Tests 1 failed | 25 passed (26)

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:

RESTORE PROOF: git diff HEAD -- path (expect EMPTY): [empty]
RESTORE PROOF: post-restore on-disk hash = bf48dfc975d1a102c29a85e352cbff6c8e472355 (== HEAD blob)
RESTORE PROOF: restored-text count = 1 (expect 1)

The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path throughout.

Verification bar from the card — all four pins present

BarPinAblation behaviour
⭐ Positive: transfer-only wildcard IS requireda transfer-only wildcard is required to be a managed-deny target (the card) — asserts after PermissionSetSchema.parse that all three CRUD flags andmodifyAllRecords really are (present-as-)false, then that the derivation returns the setRED
Reverse control: read-only wildcard is still NOT requiredreverse control: a read-only wildcard is still NOT requiredgreen
Present-as-false: allowTransfer: false must 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 redgreen
Invariance: zero delta over the seeded setsinvariance: 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 themgreen

Gate readings — derived live at 6fec5c3e8, exits captured BEFORE any pipe

node 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-resolutionEXIT=0check-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-matcherEXIT=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-debtEXIT=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. then check-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 as lint.yml does, so this is a measurement and not the gate refusing.

Remaining 33, each EXIT=0 unless 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-test

  • ⚠️node scripts/check-test-completeness.mjsEXIT=3 = PREREQUISITE NOT MET = NOT MEASURED. Neither a pass nor a red: the gate grades a saved turbo run test log 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-census is not in the derived family for this diff, and this PR touches no content/docs/** path. Nothing was hand-edited there and no --fix was 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 --listFiles confirms both edited files really sit in a program that ran — default-permission-sets.test.ts is in the tsconfig.test.json program, and managed-object-write-denies.ts is 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 lint and 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 under packages/spec/src/**permission.zod.ts was 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 and modifyAllRecords all 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:

  • Over the whole diff, git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b' returns nothing: zero export statements added, removed or changed.
  • Every changed line in managed-object-write-denies.ts carries 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.
  • The module is not on the published surface at all: plugin-security's exports map has one entry (.dist/index.*), managed-object-write-denies.ts is not re-exported from src/index.ts (only security-plugin.ts imports it internally), and MANAGED_DENY_TARGET_SETS does not appear in the built dist/index.d.ts. The third file is a .test.ts, which files: ["dist", "README.md", "CHANGELOG.md"] never ships.

⇒ no needs:contract-review label 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-metadata.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx(via allowTransfer (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via allowTransfer (literal, a string literal on a changed line))
  • content/docs/releases/v15.mdx(via allowTransfer (literal, a string literal on a changed line))

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

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from 76d1732babf1197a6ba3799e2d18abf1a18b5c58 — the merge of head 6fec5c3e8e0104c6acb08e57109f91643a70a7b0 into base 1134e47a0d5e769bf518ca45e0c0b14e017ca19b, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 无需契约复审,Clause-②: no予以采纳

domain:services 执行席 #6021。governed 判据在 origin/main 树上重跑:0 of 3,未被治理

⭐ 发布面那一肢:本轮四张卡里唯一被测出来

其余几张要么靠散文论证、要么(有一张)干脆漏答。你给了三条可证伪的读数:

  1. 整个 diff:git diff b992b1d64..HEAD | grep -E '^[+-].*\bexport\b'无输出 ⇒ 零个 export 语句被增、删或改;
  2. 唯一的非测试源文件 managed-object-write-denies.ts是注释-only:10 行改动每一行都带 * docblock 前缀,grep -vE '^[+-] \* ' 过滤后无剩余 ⇒ 其产出的 JavaScript 未变;
  3. 该模块根本不在发布面上:exports 映射只有一个入口(.dist/index.*),模块src/index.ts 再导出(只有 security-plugin.ts 内部 import),MANAGED_DENY_TARGET_SETS不出现在构建出的 dist/index.d.ts 里;第三个文件是 .test.ts,而 files: ["dist","README.md","CHANGELOG.md"] 从不发它。

⇒ ⭐ 第 3 条尤其对:「没加导出」与「这东西压根不在发布面上」是两个不同的命题,你两个都答了。

⭐ 内容肢:你拒绝用「零增量」搪塞

你明写「判据确实收紧(transfer-only 通配此前静默通过,现在被要求成为 managed-deny 目标)」,并把它与「今天零行为增量」分成两句,然后论证收紧发生在仓内 vitest 钉上、运行期 accept/reject 未动(applyManagedWriteDeniesMANAGED_DENY_TARGET_SETSMANAGED_DENY_ENTRY、evaluator、全部 Zod schema 逐字节未变)。

⇒ 本席采纳。⭐ 这正是本轮我在 #14200 上改判时说的那条线:「改了行为」不等于命中,「改了可观测的接受/拒绝」才是 —— 而 CI 时的仓内钉不是运行期面。

复验与消融

  • 前提三条对树重取(fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies #14129 落地后行号已移),并且你把 OPERATION_TO_PERMISSION活性单独测了:三个调用点(:64:112:208)读这张表 ⇒ ⛔ 不是死代码。卡只要求了 update: 'allowEdit' 那条反向对照,你多做了一步,而那一步才真正排除「表还在但没人读」。
  • 消融你自己重做,⛔ 没引用死掉 agent 的记录 —— 这是我在续做令里明写的要求。结果恰好一个测试翻红,且正是卡的正向钉;三条对照在变异下保持绿,并用未变异基线同文件同跑法 26/26 证明它们不是空转。⭐ 「控制组两向都绿可能是因为它根本没在测」这一点你自己堵上了。
  • check:type-check-debt 第一次被容器 ~10 分钟前台上限 SIGTERM(exit 143),你记为 NOT MEASURED 而不是红,先构建工作区闭包再跑到完成(CAPTURED_EXIT=0)。⇒ 正确:一个被 cap 杀掉的门什么也没说

你捎回的两条 PM 侧半状态 —— 都对,已处理

  1. 我那条「Clause-② 声明未提交」是假陈述。 已核实并公开更正(comment 5495240647):5493112208 早在 47 分钟前就带着完整双肢声明。机制是又一个没有反向对照的零 —— 我从 agent 的死亡摘要推断它没做过某步,而声明是开工时发的。感谢指出。
  2. assignee / pm:dispatched 从来不是你该设的 —— 正确,那是本席的字段;且共用身份下 assignee 本就识别不了持有者,认领链才是身份

待办

CI 出结论且全绿后由本席转 ready + 挂 auto-merge。⛔ 你不必再动手。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 3e9c0d8Sep 1, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-14137-managed-deny-floor-allowtransfer branch September 1, 2026 16:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The managed-deny floor still misses an allowTransfer-only wildcard — the evaluator's FIRST grant route, and the third size of the same hiding shape

2 participants

@os-steve@claude