Skip to content

docs(spec): the permission set name describe and the permission sets page say one holder per namespace - #33384

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-yngygz
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-yngygz

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #32758

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:权限集 name 字段的 describe 不再说「在环境的安全目录里唯一」,改成「权限集按 (包, 名字) 识别,每个命名空间一个持有者」;参考文档那一行重新生成;权限集指南里「权限集、岗位、能力每个部署一个名字」改成权限集按命名空间、岗位和能力仍按部署。
  • 为什么改:feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742(2026-11-12 合入)让两个不同命名空间的包可以各持有同名权限集,这两处文字从那时起就是错的;技能包 skills/objectstack-platform/SKILL.md 已经按 (包, 名字) 讲,没有改。
  • 风险与回滚:只改说明文字,不动 schema、规则或运行时;spec patch changeset。回滚即 revert。
  • 先看:packages/spec/src/security/permission.zod.ts 里 name 的 describe;其次 content/docs/permissions/permission-sets.mdx 改动的那一段。
  • 两分钟验证:grep -n "one holder per namespace" content/docs/references/security/permission.mdx,预期 name 行命中一次,且与 permission.zod.ts 的 describe 逐字一致。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/security/permission.zod.ts: the name describe() and its JSDoc line. FROM "Permission set name, unique within the security catalog the environment serves (lowercase snake_case)" TO "Permission set name (lowercase snake_case). A set is identified by its package and name: a name has one holder per namespace, so packages of two namespaces may each hold a set of one name, while two packages of one namespace, a package with no namespace, a platform package (base, system, sys), the environment and the platform-requested guest_portal share one set of it". The holder list is the one feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742's save-door sentence and permissionSetHoldersMeet (@objectstack/core) state. The // comment above the field gains one line pointing at ADR-0150 D2/D9 and that predicate. SnakeCaseIdentifierSchema and the field are unchanged.
  • content/docs/references/security/permission.mdx: the generated name row, regenerated (pnpm gen:schema && pnpm gen:docs in packages/spec). No other generated file carries the string; json-schema/ is untracked build output.
  • content/docs/permissions/permission-sets.mdx (the "set shipped in code cannot be edited" bullet): FROM "A set is never overridden under its own name, not even from .custom: permission sets, positions and capabilities hold one name per deployment (ADR-0146 D6)." TO: the set is still never overridden under its own name, now for the reason that holds (a permission set takes no override, ADR-0146 D6, and a set of the shipped set's name saved into the pair is refused 403 NOT_OVERRIDABLE); a permission set is identified by its package and name (ADR-0150 D2), one holder per namespace, with the one-holder cases listed; positions and capabilities keep one name per deployment.
  • .changeset/32758-permission-set-name-describe.md: @objectstack/spec patch, since the describe ships in spec dist.

Why

PR #32742 (merged 2026-11-12, 5defbfeb) made a permission-set name one holder per namespace: two packages of two namespaces may each hold a set of one name, judged by permissionSetHoldersMeet. Both texts still said one name per deployment.

The .custom sentence, measured against #32742. The order asked whether "a set is never overridden under its own name, not even from .custom" is still true. The pair's namespace is NAMESPACE_custom (the base's namespace plus _custom), a different namespace from its base, so the name rule alone would admit a second set there. A probe on this head (the real protocol over the real ObjectQL engine and an in-memory driver, the harness of protocol-custom-override-bare-slot-envelope.test.ts; base com.example.showcase, namespace showcase, shipping showcase_reader; pair created by ensureCustomizationPackage, namespace showcase_custom) answered:

Write Answer
Save showcase_reader into the pair, no overrides 403 NOT_OVERRIDABLE ("provided by a managed package and is sealed against in-place edits. Clone it under a new name…")
Save it into the pair with overrides: { package: BASE, basedOn: '1.0.0' } 403 NOT_OVERRIDABLE ("a permission takes no override (ADR-0146 D6)…")
Save it with no package 403 NOT_OVERRIDABLE (sealed)
Save it into the base 403 ITEM_LOCKED

After the refused pair save, the base's set still read "Shipped label". So the point stays true and is kept; only its reason changed. The probe was a scratch file, deleted before the commit.

Tests

  • No behaviour changes, so no test is added and nothing is ablated. The old describe string is pinned nowhere (git grep on origin/main: only the source, the generated row and released CHANGELOGs).
  • On 854b36e4: pnpm --filter @objectstack/spec typecheck exit 0; pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/security/permission: Test Files 4 passed (4), Tests 164 passed (164).

Acceptance notes

Outside this card's surface, present-tense "one name per deployment" for permission sets remains in:

  • packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts (reason: "Positions, permission sets and capabilities hold one name per deployment"). A migration entry's text, left alone per the order; in the report's open_questions.
  • packages/spec/src/kernel/customization-package.ts:251-252 (the adr_0146_d6_security_catalog basis: "D6: one name per deployment (permission sets, positions, capabilities)"). It describes ADR-0146 D6's basis as written; in open_questions.
  • packages/objectql/src/registry.ts:2818 and :2880 (the boot and hot-install refusal sentences for an environment item against a package holder). The environment still meets every holder, so the refusal stays right, but its reason says an assignment names a permission set by its bare name.
  • packages/spec/src/contracts/security-service.ts:419 (PlatformBaselineSetContribution.name, "unique in the security catalog"). A platform-baseline set lives in a platform namespace, which keeps one holder, so this stays true for what it describes.
  • ADR-0048's 2026-10-08 addendum and ADR-0146 D6 text, and the dogfood comments showcase-security.ts:32 / showcase-d7-default-profile.dogfood.test.ts:65: decision records and test comments, not touched.

skills/objectstack-platform/SKILL.md:125 already teaches that permission sets "are identified by package and name", and :129 that a permission set is never overridden; no skills change.

Verification

On 854b36e4: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 commands. 102 exit 0. pnpm --filter @objectstack/spec run check:generated exited 1 in the loop because check:test-typecheck's tsc -p died without diagnostics while other gates ran beside it; run alone after the loop it exited 0 ("All 14 generated artifacts are up to date"). pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it needs every package's dist; only the spec, lint, objectql and client closures were built), so it is NOT MEASURED here and left to CI. --ran reconciliation: "103 derived famil(ies) accounted for — 102 run, 1 NOT-MEASURED". node scripts/check-nul-bytes.mjs exit 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nvMv3rqz7e8UKwxD9

…space

A permission set is identified by its package and name, so a name has
one holder per namespace. The `name` describe, its generated reference
row and the permission sets guide say so; positions and capabilities
keep one name per deployment, and a set is still never overridden under
its own name, not even from `.custom` (ADR-0146 D6).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nvMv3rqz7e8UKwxD9
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/profiles.mdx (via PermissionSetSchema (symbol, a top-level const))

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

  • content/docs/releases/v12.mdx (via PermissionSetSchema (symbol, a top-level const))
  • content/docs/releases/v13.mdx (via PermissionSetSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-0.mdx (via PermissionSetSchema (symbol, a top-level const))

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
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 172 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 df9fe9463020a58b29a61f98605b91e7fbb307c1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from dd9a08d9d1bf4a5aa5f37078d39a0f45e84d805b — the merge of head aeb3c973e6549ffa66d66b05601f30b3af3d8cd2 into base df9fe9463020a58b29a61f98605b91e7fbb307c1, 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 dd9a08d9d1bf4a5aa5f37078d39a0f45e84d805b && git checkout dd9a08d9d1bf4a5aa5f37078d39a0f45e84d805b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df9fe9463020a58b29a61f98605b91e7fbb307c1 aeb3c973e6549ffa66d66b05601f30b3af3d8cd2 && git checkout -B drift-repro df9fe9463020a58b29a61f98605b91e7fbb307c1 && git merge --no-ff aeb3c973e6549ffa66d66b05601f30b3af3d8cd2

node scripts/docs-audit/affected-docs.mjs --json df9fe9463020a58b29a61f98605b91e7fbb307c1

⚠️ 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 df9fe9463020a58b29a61f98605b91e7fbb307c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 854b36e4fc013ffc31164226c32f2d3c29c7b67d
Local-runs: none
Scope: packages/spec/src/security/permission.zod.ts + none

No file in this diff implements or enforces the describe: the code it describes (permissionSetHoldersMeet in packages/core/src/permission-set-holders.ts, the registry's securityCatalogHoldersOtherThan, the save door's permissionSetNameCollisionRefusal and refusePackagedBaseOverride) is outside the diff and was read on main, not reviewed. content/docs/permissions/permission-sets.mdx was read sentence by sentence against the same code because the card rides it on this PR; the generated row, the changeset and that page are otherwise the lane seat's ACCEPT.

Inputs read: card #32758 body and its five comments (6837778026, 6847899833, claim 6881630925, report 6882046858, seat answers 6882071328), PR #33384 body, file list (4 files, +26 / -6) and net diff against main, PR #32742 as context, the check-runs on this head.

① Derived judgments

  • Accept set: unchanged, right. name stays SnakeCaseIdentifierSchema; only the .describe() string, the JSDoc line and a // comment move. No value parses differently.
  • Public surface: one published string changes, right. The describe ships in @objectstack/spec dist and projects to content/docs/references/security/permission.mdx. The generated name row on this head is character for character the describe string (compared on the head's two blobs). No other tracked projection carries the old string.
  • No rule, schema, gate or runtime file in the diff. Metadata-protocol: none is right: no door answers differently.
  • Docs page: one bullet of permission-sets.mdx reworded; no anchor, route or example changes.

② Semver level

.changeset/32758-permission-set-name-describe.md is @objectstack/spec: patch and carries both declaration lines (Clause-②: no, Metadata-protocol: none), matching the PR body. Patch is the right level: a description string in the published package changes and nothing an author can write or a consumer can read by shape moves. skip-changeset would have been wrong (the string is in dist). Check Changeset concluded success on this head.

Clause-②: no — agreed. Neither a widening nor a narrowing: the text now states what #32742 (Clause-②: yes (widening)) already shipped.

③ Boundary flags

  • Report open_questions Q1 (migration entry 18 reason) and Q2 (customization-package.ts:251-252 gloss): answered B by the seat (6882071328), follow-up card spec: the security-catalog migration entry and the D6 override gloss still say a permission set holds one name per deployment #33388, open. Not in this PR. Answered.
  • Out-of-scope class-b finding (objectql registry.ts refusal sentences): card objectql: the security-catalog name-conflict refusals still say a permission set holds one name per deployment #33389, open. Answered.
  • Carrier-none notes (security-service.ts:419 stays true for a platform-namespace set; ADR and dogfood comments): accepted as noted by the seat. I read :419's premise against the predicate: a holder in base, system or sys meets every holder, so "unique in the security catalog" holds for it.
  • Deviation: the // comment line added above the field. Inside the claimed file surface ("the name JSDoc and describe only" read with the field's own comment block); comment-only. Accepted.
  • Deviation: PR opened with no labels. The head now carries documentation, size/s, tooling from Auto Label. Nothing to rule.
  • Gates. The dev's local loop left check:dual-build-cjs-loads NOT MEASURED and re-ran check:generated alone. On this head, at the last read for this record, 27 check-runs concluded success (among them Check Changeset, Spec property liveness, Build Core, Build Docs, Type Check · source gates, Type Check · debt ledger, Lint & Repo Gates and every Lint · job, Check Documentation Links, Governed Surface Queue Guard, PR-body guards), 10 were skipped, none failed, and 28 had not concluded: Test Core 1/8 to 8/8, Type Check · workspace, Type Check · consumer gates, Type Check UI, Build & E2E UI, Test UI 1/16 to 16/16. This record therefore does not carry a verdict for those families; the landing seat reads their conclusions before it lands. A text-only diff gives no reason to expect a failure there, but that is an expectation, not a measurement.
  • Docs drift advisory (PR comment 6882057435) lists content/docs/permissions/profiles.mdx through the PermissionSetSchema symbol. Read on this head: it carries no sentence about a set name's uniqueness. Nothing owed.

④ Design judgment

packages/spec/src/security/permission.zod.ts — judged against the ruling it implements (card #32758's TO, ADR-0150 D2/D9 as #32742 shipped them) and its first consumer, the predicate on main.

Sentence by sentence against permissionSetHoldersMeet(incomingNamespace, holderNamespace, name):

The describe says The code on main Holds
"a name has one holder per namespace, so packages of two namespaces may each hold a set of one name" returns false only when both namespaces are declared, neither is shareable, they differ, and the name is not platform-requested yes, as the name rule (see N1)
"two packages of one namespace … share one set" last line: incomingNamespace === holderNamespace yes
"a package with no namespace" !incomingNamespace or !holderNamespace returns true yes, on either side
"a platform package (base, system, sys)" PERMISSION_SET_SHARED_NAMESPACES = base, system, sys, on either side yes (see N2)
"the environment" not the predicate's: the registry adds the environment holder without asking it (opts.environment), and the save door's meets answers true for holder === null or written === null yes
"the platform-requested guest_portal" isPlatformRequestedBareName('permission', name); the list on main is ['guest_portal'] alone yes

Attempts to break it:

  • Co-owners of one namespace: meet, second refused. Agrees.
  • No namespace beside a namespaced holder, and two with none: meet. Agrees.
  • sys + crm, either order: meet. Agrees. ADR-0150 D2 lets two packages of a shareable namespace hold one behavioural name under the sibling predicate; the describe correctly follows the stricter permission-set predicate, not D2's general wording.
  • guest_portal from crm and hr: meets whatever the namespaces. Agrees.
  • A built-in permission-set name: feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742 lists it among the one-holder cases, the describe does not. BUILT_IN_SECURITY_CATALOG_NAMES.permission is the empty set on main, so there is no such name to list. The omission is right.
  • Positions and capabilities (the // comment's last clause): the registry narrows only where type === 'permission'; position.zod.ts:84 keeps "unique within the security catalog the environment serves", still true. Agrees.

Findings:

  • N1 — nit. "packages of two namespaces may each hold a set of one name" is true of the name rule and of two packages that ship their sets (install gate and item seam), and it is not the whole answer at the metadata save door. There, (a) a set another package STORES holds its name whatever the namespaces (meets includes storedPackages.has(holder); 422 NAMESPACE_CONFLICT), the clause the save door's own sentence ends on ("and a set another package stores holds its name") and the describe drops; and (b) a shipped set that registered its item seals the bare name against any undeclared save (isSealedManagedItem(type, name) takes no package; 403 NOT_OVERRIDABLE), which is what the dev's own probe measured for the pair. The PR body's "the holder list is the one feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742's save-door sentence states" is therefore one clause short. Graded nit, not should-fix: the sentence says "may", it states the rule the predicate implements, the schema's readers are package authors whose sets ship, the guide's wording on the same point is "ship" and is exact, and the stored limit is a declared temporary one with its own card (metadata-protocol, objectql: stored permission sets of one name in two packages are kept per package in the registry, so the save door can admit a second namespace's stored set #32780, open). Accepted deviation; if the describe is touched again, "may each ship" closes it.
  • N2 — nit. "a platform package (base, system, sys)" lists namespaces as though they were packages; the predicate judges the holder's namespace. It is the save door's own shipped wording, so the two texts agree with each other. Accepted deviation.
  • N3 — nit. The // block still opens "so a name is one item in the security catalog the environment serves" (ADR-0131 D3) and is corrected only by the sentence the PR appends. Reads as a history followed by its amendment; a source comment, not published. Accepted deviation.

content/docs/permissions/permission-sets.mdx (read, the card's second target):

  • "a permission set takes no override (ADR-0146 D6)": permission is classified excluded, basis adr_0146_d6_security_catalog, cited as "ADR-0146 D6" by judgeOverrideWrite. Holds.
  • "a set of the shipped set's name saved into the pair is refused like an edit of the set itself (403 NOT_OVERRIDABLE)": an undeclared save reaches refusePackagedBaseOverride, which throws NOT_OVERRIDABLE / 403 for a sealed name when the named package is writable (the pair is); a declared one is refused by judgeOverrideWrite and thrown with the same code and status. The page's own preceding sentence gives the same code for an edit through the metadata API. Holds, and it is the reason that survives feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742: the pair's namespace differs from its base's, so the name rule alone would no longer refuse it.
  • "identified by its package and name (ADR-0150 D2)": D2's identity is (package id, type, name). Holds; the page's :268 cites D4 (the runtime keys) for the same statement, and both citations are right.
  • "packages of two namespaces may each ship a set of one name, each granted by its own package and name": the grant's permission_set_package column, described at :268-275 of the same page. Holds.
  • "Positions and capabilities keep one name per deployment": holds (above).
  • N4 — nit. The page's one-holder list leaves out the environment, which the describe names and which meets every holder at both doors. The sentence is about packages, so it is not false, but the two texts of this PR differ by that one member. Prose face: fixable here without a delta review, or left.
  • N5 — nit. Eleven lines above the new text, outside the diff, the page still says "a grant names its set by name (sys_user_permission_set.permission_set)". True of that column and incomplete beside "granted by its own package and name". Prose face, follow-up or left.

No must-fix, no should-fix. The describe, its generated row and the guide state what the predicate and the two refusals on main do in every case the card names.

Implemented-by: claude/project-thread-yngygz
Reviewed-by: session_01FEkQ5mswnWm181XgYcCUag

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

ACCEPT · PR #33384 (#32758) · head aeb3c973e6549ffa66d66b05601f30b3af3d8cd2 · 2026-11-14T19:35Z

席位核对

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 19:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit d38ceba Nov 14, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-yngygz branch November 14, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec, docs: the permission-set name describe and the permission-sets page say one name per namespace

1 participant