Skip to content

permissions 一个键承载两个互不兼容的语义,且共用 registry 同一存储槽 —— ADR-0025 授权 vs ADR-0090 权限集集合 #18031

Description

@hotlong

⛔ 未分级、未指派 —— domain:*、type、priority 是分诊的写。

缺陷

permissions 这个键有两个互不兼容的读法,而 installPackage 把它们存进 registry 里同一个槽:

读法 契约 形状 消费者
A. ADR-0025 插件能力授权 ManifestSchema.permissions = ManifestPermissionsSchema (manifest.zod.ts) string[] | { services, hooks, network, fs } PluginPermissionEnforcer,经 artifact grantedPermissions
B. ADR-0090 权限集集合 包 bundle 的 app-category 集合 PermissionSet[](带 .name / .isDefault) app-plugin.ts 的 SECURITY_FIELDS ['permissions','permission'] → metadata.registerInMemory('permission', …) → suggested-audience-bindings.ts Source 2

⇒ 一个包声明了其中一种,另一种就没地方放。

两条可验证的后果

1. 声明 A 的包,其 B 被静默丢弃(潜伏,非活)

suggested-audience-bindings.ts(plugin-security)Source 2:

const declared = Array.isArray(manifest?.permissions) ? manifest.permissions : [];
for (const ps of declared) consider(ps, manifest?.id);

consider 首行:if (!ps || typeof ps !== 'object' || !ps.name) return;

  • 结构化臂 {services,…} → 是对象但非数组 → Array.isArray 为 false → declared = [],不进循环
  • legacy string[] 臂 → 元素是 string → typeof !== 'object' → 首行 return,全部丢弃

⇒ 管理员永远收不到「把这个权限集授予 Everyone 吗?」的建议,且无任何日志。这违反本仓成文规矩「absence must be loud」(Route & surface ownership §3)。

⚠️ 当前潜伏:ADR-0025 结构化授权的生产使用量为零,所以今天没有包踩到。但它会在第一个使用 A 的包上生效,而那正是 #17147 那条路要引入的东西。

2. InstalledPackageSchema.manifest 误声明了实际存储的形状

package-registry.zod.ts 声明 manifest: ManifestSchema。但 registerApp 实际传进 installPackage 的是整包 bundle——判据在 objectql/src/engine.ts 同一函数内,installPackage 调用的前后两行分别把 manifest.datasources / manifest.objects 当作定义对象来读(ds.name)。

⛔ 更正(2026-09-13,派发席自己写错的): 本段原文写的是「而 ManifestSchema 没有这些键」——错。manifest.zod.ts:401 / :409 确有这两键,声明为 z.array(z.string())(glob 模式),且 :244 是 strictObject。⇒ 错配是类型不是缺键,而且比原文更严重:真实的 assembled 记录会被拒两次(两键 invalid_type + 其余键 unrecognized_keys)。由承接席位在复核中测出并反驳,复审席独立核实成立。

⇒ 任何按该 schema 读 registry 的消费者都可能读到与声明不符的形状。

范围建议(⛔ 非裁决,执行席自行判断)

⛔ 不要为此重命名任一侧的键——那是跨三仓的破坏性改动,而缺陷当前是潜伏的。成本最低且立刻正确的三件:

  1. 把 ✨ Set up Copilot instructions #2 的 schema 声明改成事实(或显式声明它是超集),让 InstalledPackage.manifest 不再对读者撒谎。

    ⛔ 已否决(2026-09-13),理由是一条已记录裁决,不是口味。 package-api.zod.ts:67-74:维护者 2026-09-02 取 road B「declare the assembled stage rather than widen the authoring one」,而「把 manifest 某键放宽成两阶段 union」是 road C,被点名 REJECTED(键上的 union 让两个阶段都不可校验,Prime Directive Add comprehensive test suite for Zod schema validation #12 拒绝的形状)。照本条做 = 推翻已记录裁决(Prime Directive [WIP] Add Chinese version of the documentation #13,要新 ADR 不是 changeset)。assembled 阶段已有 AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema。真正缺的只是:站在 package-registry.zod.ts 的读者拿不到指向那一侧的指针——已在 PR 里以纯声明文本补上。

  2. 让 Add metamodel interfaces for ObjectQL/ObjectUI contract #1 的丢弃变响:Source 2 遇到 permissions 是 ADR-0025 形状(非 PermissionSet[])时 warn 一次并说明它丢了什么、为什么,而不是静默返回空。
  3. 在两处契约上互相点名(ManifestSchema.permissions 的 describe / docblock ↔ app-category 集合那侧),让下一个作者在读到任一侧时就知道另一侧存在。

⚠️ 如果你核实后认为其中某条不值得做,说出来比照做有价值——请带结构性证据反驳。

溯源

在复核 #13458 正文断言「legacy 臂在 framework 有活读者 → suggested-audience-bindings.ts」时测出。该断言是误归属:那个读者消费的是 B(权限集集合),不是 A(ADR-0025 授权臂)。已在 #13458 上更正,并提示派发前重新定位 legacy 臂的真实读者。

⚠️ 本卡的自我更正史记录在 #13458 的两条评论里(先错判 Source 2 惰性,后以 engine.ts 的 installPackage 调用点推翻)——留作判读参考:别从 schema 声明推运行时形状,去看实际传参。

Refs: #13458 · #17147 · #11333 · ADR-0025 · ADR-0090

Activity

  1. hotlong commented on Sep 13, 2026

    @hotlong
    ContributorAuthor

    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 — no domain:*, 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 moves bootstrap-declared-permissions.*, permission-set-name-collision.*, permission-set-projection.ts, index.ts; I move suggested-audience-bindings.ts only). #17811 and #17814 touch 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-review goes 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 (ManifestSchema stays strictObject, both permissions arms unchanged, InstalledPackageSchema.manifest stays ManifestSchema) and adds no published export (collectDeclaredSuggestions is module-exported and is NOT re-exported from plugin-security/src/index.ts). The spec half is docblock/describe text only.

    Generated by Claude Code

  2. hotlong commented on Sep 13, 2026

    @hotlong
    ContributorAuthor

    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:

    1. Verification ran unlocked, for the reason above — the wrapper's official wording is in the PR body.
    2. The worktree objectstack-issue-18031 is deliberately NOT removed. The PR is docked under needs:contract-review on 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 --porcelain is empty and HEAD is pushed. Remove it once the review clears or the card is released.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions