Skip to content

An app-declared capability with scope: 'org' counts as a high-privilege bit, so a set carrying only app capability tokens cannot bind to the everyone anchor #17189

Description

@zhuangjianguo

Filed by the repo:hotclm PM seat on a maintainer ruling (2026-09-09, verbatim: 「平台的问题去平台修」). Filed at the destination per objectstack-ai/hotclm's AGENTS.mdPlatform gaps: report, never patch. ⛔ Not triaged: no domain:*, no priority, no assignee.

The gap in one sentence

describeHighPrivilegeBits treats any non-empty systemPermissions as high-privilege. That rule cannot distinguish the platform's own system permissions (manage_users and friends) from an application-declared capability token whose only job is to gate a navigation group inside that app — so an app cannot ship a "every employee holds this" permission set if that set also carries the token its own navigation reads.

manage_users and clm_requester.access are not the same kind of thing, and today nothing in the high-privilege check can tell them apart.

Measured on 17.3.0 (by objectstack-ai/hotclm, hotclm#11)

The app declares clm_requester as the set every employee holds (isDefault: true, which auto-binds to the built-in everyone position at boot, ADR-0090 D5), and the same set grants clm_requester.access — an app capability token, scope: 'org' per ADR-0066 D1, whose only consumer is the app's own navigation gating.

plugin-security  bindBaselineToEveryone:
  refusing to bind fallback set to everyone — high-privilege bits { offending: "system permissions" }
  → a boot warning, and NO binding

@objectstack/lint  security-anchor-high-privilege   (severity: error)
  isDefault:true suggests binding this set to the 'everyone' audience anchor,
  but it carries system permissions — the runtime will refuse the binding (ADR-0090 D5/D9)

Both symbols still present on origin/main at filing time: packages/plugins/plugin-security/src/security-plugin.ts, packages/lint/src/validate-security-posture.test.ts.

What it costs the app

The app cannot express "all employees" at all. objectstack-ai/hotclm had to fall back to a distribution model: bind clm_requester to each of its seven positions individually, and have an administrator grant the set by hand to every employee who holds no position. Every new hire is a manual step, forever, to work around a check that cannot see the difference between an app token and a platform privilege.

The maintainer there has now ruled (hotclm#11, option A) to keep that workaround and wait on this fix rather than weaken the app's design — so this card has a named downstream consumer that is actively blocked, not a hypothetical one.

Why the app should not just drop the token

The alternative the app rejected was removing clm_requester.access and gating the navigation group on authentication alone. That was refused because it hollows out the five-token *.access scheme the app's whole navigation is built on: once one group is ungated, the next author copies that. The capability model exists precisely so an app can declare its own gates; a check that punishes using it pushes authors toward not using it.

Proposed shape (not a demand — the platform seat owns the design)

The high-privilege determination distinguishes platform system permissions from app-declared capabilities with scope: 'org', and only the former blocks the everyone anchor binding. ADR-0066 D1 already draws that line at the declaration site; describeHighPrivilegeBits does not read it.

Not claimed:

  • ⛔ Not that every app capability is safe to hand to everyone — an app could declare a token that gates something dangerous. If the fix needs a narrower predicate than scope: 'org', that is the platform's call.
  • ⛔ Not that the lint rule alone is the fix. The rule mirrors a runtime refusal; both sides move together or the lint goes out of sync with the boot.
  • ⛔ Not re-measured on 17.4.0. hotclm moved to 17.4.0 today (hotclm#35), and 17.4 carries 8e0b297 (positions[] on the session payload is the SECURITY axis), which touches the same ADR family. Whether 17.4 already changes this behaviour has not been established — that is the first thing to check, and ⛔ must not be assumed either way from this card.

When this lands

The consuming app deletes seven binding lines and sets isDefault: true. The unlock criterion there is consumer-installable, not merged — hotclm consumes published @objectstack/* versions, so the fix has to ship in a release before that repo can act on it.

Related

hotclm#11 (the blocked consumer, and its ruling) · ADR-0090 D5/D9 · ADR-0066 D1 · #16110 (closed, adjacent: the lint rule keying off isDefault) · #6696 (closed, the same function's doc comment)

Activity

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions