docs(skills): document permission-set assignment, and name permissions as its defineStack key - #10402
Draft
qq9340100 wants to merge 2 commits into
Draft
docs(skills): document permission-set assignment, and name permissions as its defineStack key#10402qq9340100 wants to merge 2 commits into
permissions as its defineStack key#10402qq9340100 wants to merge 2 commits into
Conversation
…sion-set authoring site
The skills bundle taught `definePermissionSet()` without ever naming the
stack collection that registers its output, so an author reaching for
permission sets from the skills alone guesses `permissionSets` from the
factory name and is caught by the strict top level.
Every other authoring factory in the bundle is paired with its registration
key at the point of authoring (`defineStack({ translations })`,
`{ hooks }`, `{ objectExtensions }`, `{ apps }`, `{ pages }`,
`{ actions }`, `{ flows }`); `definePermissionSet` was the exception.
Bundle sweep: exactly one full `defineStack` key enumeration exists
(skills/objectstack-platform/SKILL.md), and it already names `permissions`
— so the fix belongs at the authoring site, not in the enumeration.
Fixes #10327
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T1sSZbQTnLhrK9HhNdNiB
Authoring a permission set was covered well; assigning one to a user was absent from all 11 skills in the bundle. The capability works and is correct in both directions — only the path to exercising it was undocumented, which is exactly what an enterprise evaluator reaches for when checking whether a permission story is real. Adds an "Assigning a permission set to a user" subsection to the objectstack-data security section, grounded in the platform source: - `sys_user_permission_set` is the join object. - A set declared in `permissions` is upserted into `sys_permission_set` on `kernel:ready` with a per-environment generated id (ADR-0086 D5). - `permission_set_id` takes that RECORD ID, not the `name` — the resolver loads `sys_permission_set` by `id`, so a name written there matches nothing and grants nothing, silently. Hence assignment is always two calls: resolve name -> id, then insert the grant. - Field table (`organization_id` null semantics, the half-open validity window enforced at resolution time, `granted_by` stamped by the gate, `id` minted by the driver). - Who may write the row: tenant administrator or a delegated adminScope with `manageAssignments` (ADR-0090 D12) — CRUD bits on the table are not enough. - The minimal two-user prove-the-deny flow, an ordered list of the reasons a grant can be inert, and `GET /api/v1/security/explain` as the same-code-path answer to "why". - Notes that system objects are not exposed over MCP by default, so the agent surface cannot discover this either. objectstack-platform carries the same gap check: it has no security authoring section, so it gets a pointer row in "Common ops pitfalls" rather than a second copy of the text. Fixes #10318 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T1sSZbQTnLhrK9HhNdNiB
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10318
Fixes #10327
Two members of one family from the new-developer first-run journey audit (#10264), both on the permission-set surface of the published skills bundle. One commit per member; each is independently verifiable.
skills/**only. Governed surface — draft, human merge only; no ready flip, no queue, no auto-merge.Member 1 — #10327: name
permissionsas thedefineStackkeyCommit
ee6bd9ec9.Premise: partially stale, and the correction changes where the fix lands. The card's body says the bundle's enumerated
defineStackkeys omitpermissions. Swept againstorigin/main: the bundle contains exactly one fulldefineStackkey enumeration —skills/objectstack-platform/SKILL.md:176-184— and it already namespermissions(line 180, betweenpositionsandcapabilities). So the ruled instruction "addpermissionsto every enumeration" is a no-op: the acceptance criterion "permissionspresent in every enumeration" is already satisfied, and no enumeration was edited.Sweep method (both re-runnable): a window scan over every
skills/**/*.mdfor co-occurring stack-collection keys in backticks (≥4 distinct per 3-line window), and a second scan for the audit's specific 9-key list shape. Only the platform enumeration and two deliberately-elided illustrative lists surfaced — the barrel-import example (manifest/objects/apps/views/flows) and the CRM blueprint'ssrc/{objects,views,pages,actions,flows,...}directory hint. Neither claims completeness.The card's title, however, is exactly right and still reproducible: "No skill names
permissionsas thedefineStackkey for permission sets."defineStack({ permissionsappears nowhere in the bundle. That is the real defect and it explains the audit's stumble better than the enumeration does: every other authoring factory in the bundle is paired with its registration key at the point of authoring —defineStack({ translations: [...] })in objectstack-i18n,{ apps }/{ pages }/{ actions }in objectstack-ui,{ flows }in objectstack-automation,{ hooks }and{ objectExtensions }in objectstack-data itself.definePermissionSetwas the one exception, so a dev who authored a set correctly had nothing to read and guessedpermissionSetsfrom the factory name.So the fix lands at the authoring site instead:
skills/objectstack-data/SKILL.md"Object-level permissions (RBAC)" now carries the registration comment in the code block (mirroring theobjectExtensionsconvention already used at line 565) plus a bullet naming the key, why it is named for the metadata kind rather than the factory, and that the top level is strict.The error-text claim is grounded, not recalled:
ObjectStackDefinitionSchemais built withstrictObject({ surface: 'this stack definition', … })(packages/spec/src/stack.zod.ts:202), and the message builder emitsUnrecognized key(s) on {surface}: `k1`(packages/spec/src/shared/suggestions.zod.ts:392). The doc states the error prefix and that the key is named, and does not reproduce the near-miss suggestion line verbatim.Member 2 — #10318: document assigning a permission set to a user
Commit
c43df0067. Premise valid — verified before writing:sys_user_permission_setappears exactly once anywhere in the bundle, in aobjectstack-platformtroubleshooting row that names the table without any how-to. Nothing teaches the assignment path.New subsection "Assigning a permission set to a user" in the objectstack-data security section, placed immediately after the set-authoring subsection — the narrative point where the audit's dev stalled. Every claim was read out of the platform source on this checkout, not taken from the card:
sys_user_permission_setis the join object; field list,organization_idnull semantics, uniqueness(user_id, permission_set_id, organization_id)packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.tspermissionsis upserted intosys_permission_setbynamewith a generatedidonkernel:ready(ADR-0086 D5)plugin-security/src/bootstrap-declared-permissions.ts(upsertPackagePermissionSet,genId('ps'))permission_set_idtakes the record id, not thename— a name matches nothing and grants nothing, silentlypackages/core/src/security/resolve-authz-context.ts:419-497: grant rows are read byuser_id,permission_set_idcollected, thensys_permission_setloaded byid$in;ps.nameis what lands inExecutionContext.permissions[][from, until), enforced at resolution time, no background jobisGrantActivefiltering ahead of every derivationgranted_byis stamped by the gate on insert — do not author itdelegated-admin-gate.ts:673(assertDirectGrantWrite)idmay be omitted; the driver mints onepackages/drivers/driver-sql/src/sql-driver.ts:4965(nanoidfallback increate)adminScopewithmanageAssignments+ set allowlisted + BU subtree covering the target (ADR-0090 D12); CRUD bits are not enoughdelegated-admin-gate.ts— quoted phrase is the gate's own denial textresolve-authz-context.ts§6bisRowActivefilterPOST /api/v1/data/{object}andPOST /api/v1/data/{object}/query(QueryAST in body; the path object is written last)packages/rest/src/rest-server.ts:6721-6765, and the route table already published in objectstack-apiGET /api/v1/security/explain?object=&operation=&userId=answers "why" from the enforcement code path; explaining another user needsmanage_usersor a coveringadminScoperest-server.ts:8886-8910allowSystemObjectsis setpackages/mcp/src/mcp-http-tools.ts:314-320everyoneresolve-authz-context.ts§5b, §6aAlso included: the minimal two-user prove-the-deny flow the audit ran (grant to A,
200as A,403as B, revoke, A denied), and an ordered inert-grant checklist (name-in-id-field, deactivated set, validity window, org mismatch) so the next reader diagnoses instead of guessing.objectstack-platformsame-gap check (ruled scope): that skill has no security-authoring section at all — it covers project setup, plugins and ops — so duplicating the how-to there would create the stale-second-copy class the bundle already suffers from. It gets one pointer row in "Common ops pitfalls", next to the existing row that already sends operators to this table.Exclusions honoured
sys_user_permission_setis not touched. The doc states the current behaviour (system objects are off-MCP unless opted in) because that is what stops the next stranger asking the agent and getting nothing; whether that boundary should change is a product call and stays a decision-inbox candidate, noted in the dev report, never implemented here.objectstack-platformskill still teaches that an unknown top-leveldefineStackkey is silently stripped — #8687 made it throw #9907 (the stale "unknown key is silently stripped" teaching) is untouched — that half already landed there, and this PR addresses none of it.Verification
All gates run at
c43df0067, the final commit and current head of this branch. Gate list re-derived from the actual diff withnode scripts/pm/dispatch-gates.mjs(no paths passed; the script takes its own change set from the merge base) — it named exactly the two gates the dispatch clue carried, no additions.Exit codes captured before any pipe; verdicts quoted from each gate's own output line.
check:role-word— self-test0, gate0.check-role-word: OK, no new occurrences of the reserved word./Scanned: 217 .md/.mdx file(s) read across 2 root(s) — content/docs 181, skills 36.Both edited files are baselined at fixed counts and the ratchet fails in both directions, so the added prose was written to keep the count exact:
skills/objectstack-data/SKILL.md4 → 4,skills/objectstack-platform/SKILL.md2 → 2. The reserved word does not appear in any added line.check:pm-governed-merges— self-test0(81 assertions). Predicate--teston both paths exits3= GOVERNED:governed-surface predicate: 2 of 2 path(s) hit the register/skills/** ×2 — the published skills catalog.Hence draft, human merge only.
check:nul-bytes— self-test0, gate0:OK (scanned 6117 text file(s) … no raw ASCII control bytes). Also self-scanned the two files directly withgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'— no match.check:doc-authoring— self-test0, gate0:✓ doc authoring guard: 381 files clean — no bare metadata literals.Run because the diff adds fenced blocks to the skills corpus, though the derivation scored it silent.Not run locally:
check:doc-anchors— it importsgithub-sluggerand this docs-only worktree has nonode_modules(ERR_MODULE_NOT_FOUND), an environment gap rather than a tree result. It is not implicated by this diff: the added lines introduce no](…#fragment)link, confirmed by grepping the added side of the diff. CI runs it.No changeset:
skills/**publishes nothing to npm —skip-changeset.Docs-only; no code paths change.
Generated by Claude Code