Repository navigation
permissions 一个键承载两个互不兼容的语义,且共用 registry 同一存储槽 —— ADR-0025 授权 vs ADR-0090 权限集集合 #18031
Description
Activity
Claim: PM loop — dev seat, dispatched
Session:session_680c1c4e-4867-4565-b594-b5d2662e3e65
Branch:claude/issue-18031-permissions-key-collision
Worktree:objectstack-issue-18031
Domain:domain:security(card arrives ungraded — nodomain:*, no type, no priority, no assignee; reported in the seat report)
File surface:packages/spec/src/kernel/**(manifest / package-registry declarations, docblocks only) ·packages/plugins/plugin-security/src/**(suggested-audience-bindings) ·.changeset/**(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus(default judgement tier —node scripts/pm/dispatch-gates.mjs --tier <paths>this session prints "no path-derived mandate … Clause ② SUSPECT surface", a floor hint, not a clearance)
Clause-②: yes
Thread-read: none (no comments on the card at the moment this claim is written)
Serial constraints cleared:#18022(claude/issue-17516-permission-set-collision-diagnostic) is in flight in the SAME package,@objectstack/plugin-security— its file list and mine are disjoint (it movesbootstrap-declared-permissions.*,permission-set-name-collision.*,permission-set-projection.ts,index.ts; I movesuggested-audience-bindings.tsonly).#17811and#17814touch the ADR-0090 D5 anchor semantics but neither moves a file on my surface. No in-flight claim asserts the behaviour this card pins.Clause-② reading, both limbs stated separately:
- PATH limb HITS — the diff moves
packages/spec/src/**(kernel/manifest.zod.ts,kernel/package-registry.zod.ts).needs:contract-reviewgoes on both carriers at PR-open time and the card waits outside the queue until the in-seat review clears it. - CONTENT limb does NOT hit, and the declaration above is the conservative direction rather than a claim that it does: this diff relaxes no accept set (
ManifestSchemastaysstrictObject, bothpermissionsarms unchanged,InstalledPackageSchema.manifeststaysManifestSchema) and adds no published export (collectDeclaredSuggestionsis module-exported and is NOT re-exported fromplugin-security/src/index.ts). The spec half is docblock/describetext only.
Generated by Claude Code
- PATH limb HITS — the diff moves
os-dev-report
{ "issue": 18031, "status": "done", "branch": "claude/issue-18031-permissions-key-collision", "pr": "https://github.com/objectstack-ai/objectstack/pull/18035", "premise_still_valid": true, "summary": "The card's live half is fixed and its first scope suggestion is REFUTED. collectDeclaredSuggestions source 2 now reports, once per engine per package and arm, that a package's manifest.permissions carried the ADR-0025 grant reading rather than the ADR-0090 permission-set collection it reads — naming the arm, the dropped-member count, the consequence (no suggestion row, no admin prompt, a deployment that goes on looking healthy) and the remedy (declare the sets in the package's own defineStack permissions collection). Scope item 1 was NOT done: ManifestSchema DOES declare objects and datasources (as glob patterns, so the mismatch is of type, not presence, and strictObject makes it worse than the card says), and the assembled-stage declaration the card asks for already landed at #14242 under the maintainer's road-B ruling of 2026-09-02, which rejected widening a manifest key by name as road C. What was genuinely missing were the back-pointers, which is what the spec half adds: ManifestSchema.permissions, the stack permissions collection and InstalledPackageSchema.manifest now each name the other stage. The card arrived ungraded and unassigned (no domain, type, priority or assignee).", "tests": "ALL RUNS ARE A DECLARED NARROWING — scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host (no usable flock; the lock is declared Linux-only), so every command ran UNLOCKED, declared in the PR body in the wrapper's own wording. GATES: spec build exit 0 · spec test 476 files / 13563 passed · spec typecheck exit 0 (check:test-typecheck 54 files / 259 errors / 144 pinned, unchanged) · spec check:generated — check:docs was the only stale artifact, --fix ran gen:docs, final re-run on HEAD prints 'All 15 generated artifacts are up to date' · plugin-security build exit 0 · plugin-security test 111 files / 2154 passed · plugin-security typecheck exit 0 · check:nul-bytes OK over 8604 text files · check:pm-widening-tells self-test 269 cases · check:durability-log-level 35 seams all loud, 68 read seams none inventing · check:optional-error-sink OK · check:stack-collection-maps 8 enumerations vs 31 collections · check:startup-registry-verdict 43 seams clean · check-empty-changeset exit 0 · check-adr-0087-registration exit 0 · check-changeset-no-major exit 0 · check-closing-keyword-parity OK · check-partof-closing-keyword run with the real PR body, exit 0 · check-clause2-carriers --pair 18035 exit 0, both carriers agree. Every verdict read from the gate's own printed line, never a bare exit code behind a pipe. ABLATION (run from the committed state): HEAD blob 2e6a770cd56f678cabd3fee3876d370631c32610; marker 'reportDroppedManifestPermissions(ql, logger, dropped);' present 1x in source; mutation deleted that one call and the marker count went to 0 on disk (the editor's exit status was not read as evidence); rebuilt plugin-security and ablation-dist-preflight --absent confirmed the marker gone from all 4 built files; PREDICTED red, OBSERVED red — 4 failed / 29 passed (33), exactly the four warn-asserting cases, while the three no-noise cases stayed green; restore via git checkout HEAD -- (never a bare git checkout --) put the marker back to 1, whole-tree git status --porcelain empty, git hash-object byte-identical to the HEAD blob, dist rebuilt and the preflight re-run positive. Incidental: with the call gone tsc also refuses the DTS build with TS6133 on the now-unused logger. CI on PR 18035 at report time: Check Changeset SUCCESS, Governed Surface Queue Guard SUCCESS, Spec property liveness SUCCESS, Check Documentation Links SUCCESS, claim/branch and single-writer-path guards SUCCESS; Lint and Repo Gates, Test Core 1-6, the four Type Check legs, Build Core, Build Docs, Dogfood 1-3 and Temporal Conformance all in_progress — NOT MEASURED, and not waited on.", "mcp_calls": "0 — every GitHub read and write went through the gh CLI (REST); no MCP GitHub tool was called", "open_questions": [ { "question": "packages/spec/liveness/manifest.json grades `permissions` `live` and cites this very reader, noting that it 'reads the LEGACY arm'. Measured: it ITERATES that arm and reads no value out of it — consider()'s `typeof ps !== 'object'` returns on every bare string — so the legacy arm has zero EFFECTIVE readers, exactly as the structured arm does. Does the row's status move?", "options": [ "A — leave status `live`, correct only the note to state the measured drop (what this PR did NOT do; the entry would then read as self-contradictory but honest)", "B — move the status, which is the spec seat's verdict and feeds #13458's retirement route", "C — leave the row untouched and let #11333 / #13458 carry it (what this PR did)" ], "recommendation": "C for this PR, then B in the spec seat. Moving a liveness status drives a retirement and is not a rider on a reader-side repair; the measurement is now pinned by tests and posted as evidence on #11333 so it cannot evaporate." } ], "out_of_scope_findings": [ "filed as #18034: AppPlugin's SECURITY_FIELDS loop (packages/runtime/src/app-plugin.ts) drops a manifest `permissions` it cannot read at BOTH arms with no log — the same collision, the second consumer, and the half that runs at every boot for every stack", "noted, not filed: packages/spec/liveness/manifest.json's `permissions` row calls the legacy arm live on a reader that discards every value of it; carrier is #11333 (evidence comment posted there) and its retirement route is #13458, so a duplicate card was not opened" ] }Two deviations, declared rather than silent:
- Verification ran unlocked, for the reason above — the wrapper's official wording is in the PR body.
- The worktree
objectstack-issue-18031is deliberately NOT removed. The PR is docked underneeds:contract-reviewon both carriers and is waiting on an in-seat contract review of this same branch, so a patch round on this claim is a named, expected next step rather than a finished landing. Nothing is uncommitted in it:git status --porcelainis empty and HEAD is pushed. Remove it once the review clears or the card is released.
Generated by Claude Code
- added a commit that references this issue
on Oct 9, 2026
⛔ 未分级、未指派 ——
domain:*、type、priority 是分诊的写。缺陷
permissions这个键有两个互不兼容的读法,而installPackage把它们存进 registry 里同一个槽:ManifestSchema.permissions=ManifestPermissionsSchema(manifest.zod.ts)string[]|{ services, hooks, network, fs }PluginPermissionEnforcer,经 artifactgrantedPermissionsPermissionSet[](带.name/.isDefault)app-plugin.ts的SECURITY_FIELDS ['permissions','permission']→metadata.registerInMemory('permission', …)→suggested-audience-bindings.tsSource 2⇒ 一个包声明了其中一种,另一种就没地方放。
两条可验证的后果
1. 声明 A 的包,其 B 被静默丢弃(潜伏,非活)
suggested-audience-bindings.ts(plugin-security)Source 2:consider首行:if (!ps || typeof ps !== 'object' || !ps.name) return;{services,…}→ 是对象但非数组 →Array.isArray为 false →declared = [],不进循环string[]臂 → 元素是 string →typeof !== 'object'→ 首行 return,全部丢弃⇒ 管理员永远收不到「把这个权限集授予 Everyone 吗?」的建议,且无任何日志。这违反本仓成文规矩「absence must be loud」(Route & surface ownership §3)。
2.
InstalledPackageSchema.manifest误声明了实际存储的形状package-registry.zod.ts声明manifest: ManifestSchema。但registerApp实际传进installPackage的是整包 bundle——判据在objectql/src/engine.ts同一函数内,installPackage调用的前后两行分别把manifest.datasources/manifest.objects当作定义对象来读(ds.name)。⇒ 任何按该 schema 读 registry 的消费者都可能读到与声明不符的形状。
范围建议(⛔ 非裁决,执行席自行判断)
⛔ 不要为此重命名任一侧的键——那是跨三仓的破坏性改动,而缺陷当前是潜伏的。成本最低且立刻正确的三件:
把 ✨ Set up Copilot instructions #2 的 schema 声明改成事实(或显式声明它是超集),让InstalledPackage.manifest不再对读者撒谎。permissions是 ADR-0025 形状(非PermissionSet[])时warn一次并说明它丢了什么、为什么,而不是静默返回空。ManifestSchema.permissions的 describe / docblock ↔ app-category 集合那侧),让下一个作者在读到任一侧时就知道另一侧存在。溯源
在复核 #13458 正文断言「legacy 臂在 framework 有活读者 →
suggested-audience-bindings.ts」时测出。该断言是误归属:那个读者消费的是 B(权限集集合),不是 A(ADR-0025 授权臂)。已在 #13458 上更正,并提示派发前重新定位 legacy 臂的真实读者。engine.ts的installPackage调用点推翻)——留作判读参考:别从 schema 声明推运行时形状,去看实际传参。Refs: #13458 · #17147 · #11333 · ADR-0025 · ADR-0090