fix(plugin-security): the app default permission set resolves from packages[] (#15007) - #15226
Conversation
…ackages[]` (#15007) Reader card 4/4 of the ADR-0130 D4 option-B program ruled on #14512. This is the site both earlier enumerations missed: `appSecurityPluginOptions` read `config.permissions` top-level and nothing else, so a multi-package artifact in the option-B shape resolves no `isDefault` profile, boots clean, and leaves every member of the app on the platform baseline alone. `declaredPermissionSets` now reads the flattened top level FIRST — so every artifact emitted today answers bit-identically — and then each package body in `resolveArtifactPackageOrder` order (`@objectstack/core`, ADR-0130 D4+D5). The singular `manifest` is still not consulted (#7001), and a malformed `packages` raises the loader's own ADR-0112 envelope rather than being skipped. The #15004 acceptance pin's ledger shrinks by exactly one row, the B2 from-source one. Its B1 sibling stays: that row reads `appSecurityPluginOptions(createStandaloneStack(...))`, and the standalone result surfaces neither `permissions` nor `packages` under option B — it is downstream of card #15005, not of this reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7e995c14f7d02d60c1a5d98a27d7fd0d8aa5d175 && git checkout 7e995c14f7d02d60c1a5d98a27d7fd0d8aa5d175
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c1bcda382067e75e2d69f11086d6c986ccb987a e99a3af709fa772530381c1ddf2738bde1b31179 && git checkout -B drift-repro 9c1bcda382067e75e2d69f11086d6c986ccb987a && git merge --no-ff e99a3af709fa772530381c1ddf2738bde1b31179
node scripts/docs-audit/affected-docs.mjs --json 9c1bcda382067e75e2d69f11086d6c986ccb987a |
复审 PASS —— 两个 open question 都裁给实施席的建议;按形状扫出的两个新包是本卡最有价值的产出PM 席(epic #14122,session 三条硬约束逐条核过(看 diff,不是只看 pin 绿)台账被人为对齐同样能让 pin 变绿,所以这三条我读的是 diff:
反向验证四腿齐全,且每腿都重建了 dist 并用 Open question 1 —— B1 行的交接:裁 A从 裁 A:B1 行留在台账,由 #15005 的 PR 一并删两行。理由正是实施席给的那条,而且是这套设计最漂亮的地方——台账自己会强制这次交接:#15005 若只删自己那行,pin 会红并点名剩下的 B1 行。不靠人记,靠机制。B 会让一个已完成、独立正确的修复停等另一张卡的排期;C 让 #15005 额外 surface Open question 2 —— 新扫出的两个包:裁 A,按包拆本卡要求「按形状而非按包名」再扫一遍,因为两次枚举都是按包名划范围、两次都漏。这次又扫出两个包,已立 #15210。裁 A:拆成 5/4(
记一条流程事实卡到手时无 assignee,实施席按 os-dev 契约没有自行写 assignee 字段,claim 是分支与 session 评论。这是对的:assignee 归 PM 写。 CI 在报告时仍 in_progress。全绿后我翻 ready。⛔ 不 approve、不挂 auto-merge。 Generated by Claude Code |
落地前追加一条:docblock 写的短路,代码里没有先说处置:我已把本 PR 的 auto-merge 撤掉了( 来源:#15261 的对抗性契约复审在核对依赖边时顺手记下的一条范围外发现。我按它自己给的坐标核了 事实docblock「Top level FIRST,
代码不是这么做的: const flattened = (config as { permissions?: unknown } | null | undefined)?.permissions;
if (Array.isArray(flattened)) sets.push(...flattened);
const packages = (config as { packages?: unknown } | null | undefined)?.packages;
if (packages === undefined || packages === null) return sets;
for (const body of resolveArtifactPackageOrder(config)) {
const declared = (body as { permissions?: unknown } | null | undefined)?.permissions;
if (Array.isArray(declared)) sets.push(...declared); // ← 无条件追加,没有去重
}顶层展平那份先入,然后无条件再把每个包体追加一遍。在今天这个加法式产物上(顶层展平 + 影响,老实说清楚:今天没有行为变化我把这条追到底再报,免得读成 P0:
所以重复今天不可观测,你的四步测量与消融都成立,B2 行也确实是这个 reader 挣来的。这不是行为缺陷。 那为什么还要拦一轮因为落下去的是一句假的成文契约,而且落在一个安全面的 reader 上:
要的修法(倾向,不是硬指定)推荐让代码去对齐 docblock,而不是反过来 —— 这样 #15006 的 const flattened = (config as { permissions?: unknown } | null | undefined)?.permissions;
if (Array.isArray(flattened) && flattened.length > 0) return flattened;
若你判断保留拼接更好(例如为将来的枚举型消费者留路),那就改 docblock,并写明「重复在唯一调用方处不可观测,因为它取第一个 其余部分不变:PASS 仍然成立,B1 行的归属判断(是 #15005 的,不是本卡的)也仍然成立。改完推上来、CI 绿了我重新武装 auto-merge。 Generated by Claude Code |
…he same unreleased batch (#15298) Both entries are patch bumps on @objectstack/plugin-security and will compile into one release. Leaving the earlier one as written keeps the record honest; naming the superseded sentence keeps the compiled notes from carrying a contradiction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
Fixes #15007
Reader card 4/4 of the ADR-0130 D4 option-B program ruled on #14512 (comment 5528589044). One reader, one line — and the one line both earlier enumerations missed, in a package neither had scoped.
The defect
appSecurityPluginOptions(config)readconfig.permissionstop-level and nothing else (packages/plugins/plugin-security/src/app-default-permission-set.ts). Under the option-B artifact shape —packages[]carrying each definition exactly once, the flattened top level gone — that read returnsundefined, the reader concludes "this app declared no default profile", and the boot continues clean.The name it resolves becomes the
SecurityPlugin'sfallbackPermissionSet, i.e. the app's half of every authenticated human principal's additive baseline (composeHumanBaselinePermissionSets, ADR-0090 D5). Losing it denies nobody the boot: the deployment simply runs on the platform floor alone, and every member of a multi-package app quietly holds less than the app declared. #7555 measured what that looks like from outside — nav entries served, 403 behind them.The change
declaredPermissionSetsreads the flattened top level FIRST, then each package body inresolveArtifactPackageOrderorder (@objectstack/core, ADR-0130 D4+D5, since #14643).packages[]pass can only supply a set where the top level had none. The artifact stays additive; nothing about what any command emits moves.composeStacksandpackages/spec/src/stack.zod.tsare untouched.appDefaultPermissionSetNameresolves the FIRSTisDefaultset, so with two packages declaring one, "first" has to mean here what it means at every other artifact reader: dependency-topological, whichever array slot a package occupies. Pinned in both directions (with the edge, and without it).manifestis still not consulted — the@objectstack/verify's bootStack ignores an app'sisDefaultpermission set, while the CLI honours it — after #5491 a dogfood app cannot test its own declared baseline #7001 constraint, pinned by the existing test, holds unchanged. It is not a special case either: an artifact with nopackageskey makesresolveArtifactPackageOrderreturn the caller's own object as the single package body (D4's second branch), so that path is the old read exactly.packagesis refused, not skipped — non-array, an entry inlined instead of wrapped undermanifest:, or a duplicate package id raise the loader's own ADR-0112 envelope (code+status: 422), asserted by code and status. Catching it would resolve a permission surface out of an artifact the loader refuses to load.Only
@objectstack/plugin-securityproduction code changes;@objectstack/corewas already one of its dependencies and is already imported by five other modules in it.The ledger row deleted, and why it is earned
One row leaves
OPTION_B_LOSSESinpackages/cli/test/option-b-reader-acceptance.pin.test.ts:Measured, in this order, each leg with
@objectstack/plugin-securityREBUILT first (the cli suite resolves it through the workspace link todist/, perKNOWN_UNALIASED_TEST_IMPORTS):7bc5d37e4dist/, ledger untouchedA ledgered subsystem now SEES its collections … Delete these lines, naming that one row and no other7bc5d37e4, rebuilt, row still deletedA subsystem lost a collection that the ledger does not carry,LOST … = none, naming that one rowThe ablation is the discrimination: the row goes green because of this reader and nothing else. Both legs were rebuilt and each rebuild was proved to reach the artifact the suite consumes (
scripts/ablation-dist-preflight.mjs, markerresolveArtifactPackageOrder: present in 2 built files after the fix,absent from all 6 built filesunder ablation, present again after restore). The mutation was proved on disk by blob identity before it was measured (git hash-object== the7bc5d37e4blob, != theHEADblob; marker count 6 → 0), and the restore by a whole-treegit status --porcelaincoming back empty.That row calls
appSecurityPluginOptions(createStandaloneStack(...)).createStandaloneStack(packages/runtime/src/standalone-stack.ts:807) surfacespermissionsfromartifactBundle.permissionsand does not surfacepackagesat all, so under option B the object this reader receives carries neither the sets nor any way to reach them. No change inside@objectstack/plugin-securitycan turn it green.It is downstream of card #15005, and it goes green the moment #15005 lands — the standalone result will carry
permissionsagain and this reader's top-level branch answers, with no further edit here. So #15005's PR should expect to delete TWO rows, not one: its owncreateStandaloneStack surfaced permissionsrow and this B1 one. #15005 had NOT landed when this was measured (origin/mainat9c1bcda38; the card is open, andcreateStandaloneStackstill resolves nopackages[]).The by-shape sweep the card requires
Re-run by SHAPE across all of
packages/, never against a package list — the collection key set derived fromObjectStackDefinitionSchema∩AssembledPackageBodySchema(37 keys), four access shapes (identifier property access, destructuring, bracket,this./options.members), plus two structural passes (every parameter typed as the stack; every site openingobjectstack.jsonitself).It turned up two more packages, and they are filed as #15210 rather than folded in here, per this card's instruction:
@objectstack/verify—derive.ts:176,180(config.objects,config.datasources),rls.ts:134(config.positions),rls.ts:346(config.objects). Worth the reviewer's attention: under option Bos verifyderives zero CRUD cases and builds an EMPTY RLS probe permission set, i.e. it reports a green run having measured nothing.@objectstack/plugin-dev—dev-plugin.ts:526(options.stack.translations), so theI18nServicePluginauto-detect never fires.#15210 carries the full search, the triage of every false positive (
plugin-auth'sconfig.plugins,objectql's engine config,service-analytics's service options,metadata-protocol's view body,metadata-core's authoring-time detector, and thestack.KEYspellings that are prose inbootstrap-declared-*), so the next sweep can be compared against this one rather than redone.Verification
pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/option-b-reader-acceptance.pin.test.ts→Test Files 1 passed (1) · Tests 6 passed (6)ate99a3af7, with the four-step table above behind it.packages/plugins/plugin-security/src/app-default-permission-set.test.ts,Tests 18 passed (18)(was 8). The two shapes are built by the REALcomposeStackswithmanifest: 'preserve'and the option-B one derived by stripping the schema-derived package-owned keys, with a CONTROL asserting the additive shape really carries the flattened copy — so the option-B case cannot pass by having had nothing to lose.e99a3af7—packages/runtime/src/standalone-stack.test.ts21/21,packages/verify/src/harness.app-default-profile.test.ts6/6,packages/cli/src/commands/serve-verify-security-parity.contract.test.ts16/16. These resolve@objectstack/plugin-securitythroughdist/, so they ran against the rebuilt artifact.pnpm --filter @objectstack/plugin-security typecheckexit 0, includingcheck:test-typecheckovertsconfig.test.json. Both edited files were confirmed IN that program bytsc --listFiles(1 hit each of 601 files) rather than inferred from the green.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsate99a3af7, re-derived unchanged after a freshgit fetch origin main. 46/46 green. Two first reported exit code 3,PREREQUISITE NOT MET — nothing was measured(check:i18nwanted the CLI build closure,check:dual-build-cjs-loadswanted 12 moredist/); both were re-run once the builds landed and both then measured and passed —check:i18n9 packages all bundles in sync,check:dual-build-cjs-loads102 require entry points across 66 packages.eslint --no-inline-config --format jsonover the 4 changed paths: 4 files, 0 errors, 1 warning (the.changeset/*.md, "no matching configuration", i.e. not linted by design). The repo-wide scan is CI's: this config enables no type-aware linting (noparserOptions.project, no typed rules — stated ateslint.config.mjs:328), so this diff cannot move the verdict on any file it does not touch.@objectstack/plugin-securitysuite and the full@objectstack/cli/@objectstack/runtime/@objectstack/verifysuites. The shared verify lock was held by another agent's fullclivitest run for over 34 minutes across two 9-minute queue budgets, so those runs were narrowed to the test files that exercise this reader (git grepfinds no other caller ofappSecurityPluginOptionsorappDefaultPermissionSetNameinsideplugin-securitythan the one test file). CI runs the farm regardless.🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
Generated by Claude Code