Skip to content

feat: the position list says who holds each position and which permission sets it grants - #25369

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-kuevbz
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-kuevbz

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #25333

Clause-②: yes (widening)
Metadata-protocol: none

What

Setup → Positions can now say who holds each position and which permission sets it grants. Both answers come from the rules the authorization resolver enforces with. They are not a second copy of those rules.

  • GET /meta/position?include=grants adds two read-only keys to each position: _holderCount and _grantedPermissionSets: [{ name, via }]. Both transports serve it, through the shared list chain (createMetaListAnswer). Without ?include=grants, the list is byte-for-byte unchanged, so the pickers and assignment pages that read it pay no extra read.
  • via: 'definition' marks a set the position's permissionSets names. via: 'name' marks the set that shares the position's name, which the position-name fold grants although the definition does not list it. That is the set Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757 found in force and invisible. The fold's retirement is on hold, so the list shows it rather than hiding a grant that is live.
  • _holderCount counts holders in the caller's active organization by the rule the resolver uses to give a caller positions: sys_user_position rows in their validity window, membership roles, and everyone for every active member. A switched-off position keeps its holders (ADR-0049). The count is null, never 0, in these cases:
    • there is no active organization;
    • the position is platform_admin or guest;
    • the read was truncated;
    • the caller may not read sys_user_position.

How

  • @objectstack/core. Two readers are lifted out of resolveUserAuthzGrants: readPositionGrantsInForce (the §6a rule: the definition's sets in force, with disabled positions and sets dropped) and countPositionHolders, which uses the §3/§4 holding rule through the shared heldPositionNames. The resolver now calls the same helpers and resolves exactly what it resolved before; the core security suite is green.
  • @objectstack/spec. ISecurityService gains the optional describePositionGrants(positions, context) and its PositionGrantSummary type. A security service without it serves the list undecorated, never with empty values.
  • @objectstack/plugin-security. It serves describePositionGrants by running the evaluator's own resolvePermissionSets plus dropDeactivatedFoldedSets over [position, ...declared], the same steps resolvePermissionSetsForContext takes. The holder count is gated on canReadObject('sys_user_position', context).
  • @objectstack/rest. Step 8 of createMetaListAnswer decorates a position list when asked with include=grants. An anonymous caller, or a service without the method, gets the list unchanged.

Acceptance (the card's Done when)

  • Holder count and sets per row. Served as above. The console half (the columns and the whole-row click) is objectstack-ai/objectui's own draft PR on the same branch name, claude/project-thread-kuevbz.
  • Matches what the server enforces. This is pinned as agreement, not as the reader's own output:
    • position-catalog-readers.test.ts checks that, user by user, countPositionHolders counts a user for P exactly when resolveUserAuthzGrants gives that user P, and that a position's sets in force are exactly what the resolver grants a holder of it alone.
    • describe-position-grants.test.ts checks that each described position's sets equal resolvePermissionSetsForContext for a holder of that position alone, the same-named set included.
  • Same-named set case. sales_manager lists crm_sales_user and forecasting (via: 'definition') and sales_manager (via: 'name'). Three controls go with it:
    • a set declared under the position's own name is a declaration, not a fold;
    • a fold whose set is switched off grants nothing;
    • guest folds nothing.
  • Both transports. meta-list-projection-parity.test.ts runs the ?include=grants probe through RestServer and the runtime dispatcher.

Ablations

Each was run with scripts/ablation-replace.mjs and restored to the HEAD blob.

  • Removing the fold from describePositionGrants turns 2 tests red.
  • Removing the canReadObject gate on the holder count turns 1 test red (the null-for-a-member-without-the-read case).

🤖 Generated with Claude Code

https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD

Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD
Co-authored-by: Claude <noreply@anthropic.com>
…sion sets it grants

GET /meta/position?include=grants decorates each position with
_holderCount and _grantedPermissionSets, from the security service's
describePositionGrants, which reads both where the resolver enforces them.

Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d12e650ded11983272a0ebda8c75d962573ebb85.

⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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 — 154 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 d12e650ded11983272a0ebda8c75d962573ebb85 → packageMentionDocs.

Which tree this was computed on

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

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

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

…WHERE combinator

check:where-matcher found that the double's matcher read a combinator key as a
field name. The double now throws on any `$`-led key, so a read it does not
implement fails loudly instead of matching wrongly.

Claude-Session: https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:engine seat 1 · os-tesla · session_014Hse9cj2i4o5UAVrd21NKr · 2026-10-23T05:31Z · head 0ca1af8c1 · #25333 (Fixes, with the console half objectstack-ai/objectui#13351 landing in the same stroke)

  • Shape: 12 files, +783 / −26: core resolve-authz-context.ts (two readers lifted out of the resolver, plus readPositionGrantsInForce / countPositionHolders), the optional ISecurityService.describePositionGrants in the spec contract, its plugin-security implementation, the opt-in include=grants projection in rest meta-item-read-gate.ts, and pins. Read from the diff. NOT GOVERNED, under the 3000-line threshold.
  • Clause-② (yes) seat review, TIER_DEFAULT: the widening is opt-in. A plain GET /meta/position is unchanged, and include=grants adds _holderCount and _grantedPermissionSets. The contract method is optional, and the consumer feature-detects it and serves no description when it is absent, never an empty one. No authorable key is added.
  • Enforcement path, read for equivalence: the resolver's §3/§4 position projection now calls heldPositionNames, which keeps the same filters in the same order (active window, tenant match, role split and map, then grantAppliesInTenant). §6a now calls setsInForceByPosition, which drops a deactivated bound set before it is taken instead of taking it active: false. Only active rows ever reach foldCatalogSetBodies, and it is a no-op on an empty list, so every principal's grants are unchanged. The tests pin agreement with resolveUserAuthzGrants and resolvePermissionSetsForContext.
  • Disclosure: the holder count is served only when the caller may read sys_user_position in an active organization, and is null otherwise. The set names a position grants are already in the position definition the caller can read. The via: 'name' fold entry is derived from the position's own name.
  • Cross-lane: spec (contracts/security-service.ts, contract JSON) was declared on [PM seat] domain:spec · seat 2 — ⏳ vacant #18549 and plugin-security on [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021. No objection was posted.
  • Gates: 42 check-runs on 0ca1af8c1: 35 success, 7 skipped (opt-in or path-filtered), 0 failed. The dev ran 97/97 derived families with 0 NOT-MEASURED; check:where-matcher caught a defect in a test double, which was fixed.
  • Pins: ablations turn red as expected: removing the fold turns 2 tests red, and removing the read gate turns 1 red.
  • Accepted deviations: (1) the landing is core / rest / spec contract / plugin-security, not metadata* / platform-objects, because that is where the rule the resolver enforces lives. (2) In objectui#13351 the whole-row click applies to every Setup metadata list, guarded against control, modified, selection and portal clicks. Both are consistent with the card's intent.
  • Landing: both halves are marked ready and auto-merge is enabled. The console half shows "—" against a server without include=grants, so landing order is free.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 05:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 05:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 27b3f1f Oct 23, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kuevbz branch October 23, 2026 06:10
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 28, 2026
…ssion sets; whole row opens the item (objectstack-ai#13351)

<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoWnNJDmFSk7m68Yejj9bevh)_

Part of objectstack-ai/objectstack#25333. This is the console half. The
server half is objectstack-ai/objectstack#25369.

- [x] Adds no product-specific widget, dialog, hook or string (see
AGENTS.md › Product-neutral renderer)

## What

Setup → Positions now says who holds each position and which permission
sets it grants, and the whole row opens the position.

- **Two columns, both the server's.** The list reads `GET
/meta/position?include=grants` through a new `listInclude` registry
option. **Holders** renders the served `_holderCount`, and **Permission
sets** renders the served `_grantedPermissionSets` in served order. The
server computes both where the authorization resolver enforces them
(objectstack#25369), and the console computes neither.
- **The same-named set is shown, marked "(by name)".** The position-name
fold grants it although the definition does not list it
(objectstack#24757). A tooltip says why it is there. Declared sets carry
no mark.
- **A count nobody measured is "—", never 0.** That covers a served
`null` and a server that predates `include=grants`.
- **The whole row opens the item** on every Setup metadata list
(`openRowFromClick`). The first cell keeps its link for the keyboard,
screen readers and opening in a new tab. These clicks do not open the
row:
  - a click on a control in the row (the Active switch, Clone, a link);
  - a modified click;
  - a click that ends a text selection;
- a click that reached the row through a portal (a dialog a row control
opened).
- **`@object-ui/data-objectstack`.** `MetadataClient.list` and
`listResponse` accept `include: 'grants'`.

## Tests

- `ResourceListPage.positionGrants-25333.test.tsx` (5 tests):
- the position list asks `include: 'grants'`, and the permission-set
list (control) does not;
  - counts as served, with "—" for `null` and for an absent key;
- sets in served order, with `via: 'name'` marked and declared sets
unmarked;
- a row click opens the position with its scope kept, and (control) the
Active switch and a modified click do not navigate.
- `metadata-client.listInclude-25333.test.ts` (3 tests): the URL with
and without `include`, alongside `package`.
- **Ablations**, each turning exactly its test red:
  - with no control guard, the switch click navigates;
  - with no row handler, the row click does nothing;
  - with no `include`, the list asks the plain list.
- **Locally green:**
  - type-check for app-shell and data-objectstack;
  - eslint on the touched files;
- `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`,
`check:new-line-citations`, `check:vi-mock-specifiers` and
`check:test-path-roots`;
- vitest on `packages/data-objectstack/`,
`packages/app-shell/src/views/metadata-admin/` and
`packages/app-shell/src/__tests__/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01K9LFkQaGHiensHNgGGgKvD

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants