Repository navigation
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
Description
Filing gate ③ a task the maintainer set: objectstack-ai/hotcrm#2188, the maintainer's browser pass of 2026-10-20. Each defect gets its own card, by the maintainer's ruling 「按照按照具体的issue单独提交卡片,不受数量限制。」 (recorded on objectstack-ai/hotcrm#2188, 6283467378). This is finding F-02 of track 2 (Setup as the system administrator), by the cloud tester session_01VuXyJvhGKJy8qeUvhKyxzv. It was measured on HotCRM main f22dfa52 with @objectstack/* 18.0.0-next.5, in Chromium at 1440×900 on a locally booted, seeded install. The evidence is on the branch claude/qa-setup-pass-2026-10-20 at ed4f40e42, which is never merged. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack triage routes it.
The finding, as measured
- Persona / URL: dev admin;
/_console/apps/setup/metadata/position/sales_managerand/_console/apps/setup/metadata/permission/sales_manager. - Steps: open position
sales_manager; open permission setsales_manager; sign in as Marta Quinn (holds only thesales_managerposition, nosys_user_permission_setrow). - Expected: the position page's own text, "Positions don't grant data access on their own. Bind
sales_managerto one or more Permission Sets", and its Permission Sets field (empty) describe what holders get; the permission set's Assigned Users lists who holds it. - Actual: the position shows no bound set and says it grants nothing; the permission set shows "Assigned Users 0 — No users assigned yet". Yet Marta reads all 9 accounts, which only
sales_manager'sviewAllRecordsoncrm_accountgives. The platform binds a position to the permission set of the same name (recorded insrc/sales/sharing/demo-staffing.ts, feat(analytics): server-side totals for matrix reports (subtotals + grand total) #1779). The same holds forsales_rep(Assigned Users lists only the tester's direct grant, not the two reps). - Evidence:
14-F-permset-sales-manager-assigned-users-empty.png,12-access-positions-detail.png,14-access-permission-sets-sales-rep.png;GET /api/v1/data/crm_accountas Marta → 9 records. - class: b (who holds View All is not visible to the admin). Reach: every admin of an app that relies on name-matched position bindings, HotCRM included.
- Suspected landing: objectstack
consolemetadata pages for position and permission (Assigned Users / Permission Sets panels), against the name-matching binding inplugin-security. - Dedupe words: implicit position binding, same-named permission set, assigned users empty, positions don't grant data access.
Seat review
- Seat's class: a, not b. Nothing is granted that should not be. The gap is that an admin cannot see who holds the set. It is an audit-visibility defect.
- Landing (seat review):
plugin-securityserves no effective holders for an own-name position→set binding, and the console panels read only explicit bindings.
Dedupe
Method: semantic search_issues on objectstack-ai/objectstack and objectstack-ai/objectui, open and closed, using the finding's own dedupe words and a second phrasing; plus a grep of every issue title (objectstack #1–#24751, objectui #7–#13153) listed through the repo-scoped issues?state=all endpoint, because the semantic search can miss title words (objectstack#14743).
- "same-name position permission set" family: 8 + 6 hits in objectstack, 3 + 3 in objectui. Related: objectstack#24736 (open), where the delegated-admin gate misses the same own-name binding, a different consumer of the same rule. objectui#2382 (closed 2026-07-10) fixed explicit bindings only. New.
Generated by Claude Code
The retirement, now 18.x (domain:services seat 2)
(「甲」 amended by the maintainer, 6303865025 on #6015: a narrowing that no conversion carries also ships now, in 18.x, as a BREAKING minor; no refusal waits for 19.0.)
Blocked-by: objectstack-ai/hotcrm#2200
The 18.x warning landed as PR #24988 (f90ea677c). What remains is the fold's retirement, per this card's decision 6290781571 (its 裁后执行 for 提前): HotCRM declares permissionSets on its five positions first (objectstack-ai/hotcrm#2200), so the retirement revokes nothing; then the seat dispatches the retirement: the position-name request and dropDeactivatedFoldedSets removed, the warning re-pointed to 'not granted; declare permissionSets', and the delegation walk, explain and the collision gate follow. The gate wiring in .github/workflows/lint.yml makes that PR Tier H, a draft for the maintainer's approval.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 2 · 2026-10-20T12:02Z
Session:
session_01UmqzD97e14iws6hmVhgZ5yAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24757-own-name-set-visibility(a readings-only dispatch: no edit, no PR; the readings are posted to this card)Worktree: none (readings on
origin/mainfrom the shared checkout, read-only)Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface: none written. Read:
packages/plugins/plugin-security/src/**(the position → same-named permission-set fold and its consumers, including #24736's delegated walk), the objects and panels behind Setup's position page and the permission set's Assigned Users (objectstack and objectui at the console pin), HotCRM's reliance on name-matched bindings (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(TIER_DEFAULT; a readings-only dispatch whose output is a decision analysis)Clause-②: no
Responsibility:
plugin-security folds a position into the permission set of the same name, while Setup's panels read only explicit bindings | none known | every admin of an app that relies on name-matched position bindings; measured on a HotCRM install 2026-10-20 (card)Thread-read: none
Serial constraints cleared: nothing is written. Seat 1's #24736 (the delegated walk counts the own-name set) landed as PR #24793.
Per triage
6283779151on #24736: read both sides before choosing between making the own-name binding visible and retiring the fold (#13419). Retiring it is a behaviour change, so its timing is the maintainer's.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24757,
"status": "reading",
"branch": "claude/issue-24757-own-name-set-visibility (named by the claim; never created or pushed, a readings-only dispatch)",
"pr": null,
"session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "The card's premise holds on objectstack origin/main 81120ca: plugin-security folds every held position name into the permission-set request (security-plugin.ts:6830), so a position grants the same-named set with nothing declaring it. Setup cannot show that grant, because both of its panels read the declared binding, the position definition's permissionSets, which spec and docs say is the only binding. Only the undeclared fold grants more than that. The measured reliance on the fold is one app: HotCRM, where 5 of its 12 positions have a same-named set and none declares permissionSets. objectstack's built-ins and examples have zero name collisions. The fold's retirement was ruled on #13419 (2026-08-31, B). It was never executed: the deletion was parked, and the card closed on ADR-0131. My reading recommends option C (retire the fold), pulled into 18.0 and sequenced HotCRM-first: HotCRM declares its 5 bindings, then objectstack deletes the fold. Done that way, #24757 closes with no objectui change. The question is the maintainer's, under 「是否提前」.",
"readings": {
"anchors": "objectstack origin/main 81120ca (the dispatch anchor). The shared checkout's origin/main moved to 6b9e6df mid-reading, after a sibling's fetch. The one commit between them touches only packages/services/service-datasource, and every file cited here is unchanged, re-checked by symbol at both. objectui at the console pin 616df6598b (.objectui-sha). HotCRM at f22dfa52, the card's sha: the local checkout is shallow and its origin/main is behind. Remote main is 9db9a58b; a REST compare from f22dfa52 to 9db9a58b changes no position, permission, sharing or profile source, only the profiles docs.",
"1_the_fold": {
"site": "packages/plugins/plugin-security/src/security-plugin.ts:6830, inside resolvePermissionSetsForContextUnmemoized: const requested = [...positions, ...explicitPermissionSets]. The guest is excluded at :6827-6828 (ADR-0138 D3).",
"helpers_that_exist_only_for_it": "dropDeactivatedFoldedSets :6917, which drops a folded set that the activation ledger switched off. reportNameFoldCollisions :6987-7026, which emits the runtime warn position_name_fold_grant (const :917). It fires once per name per plugin instance, and its text tells the operator to declare permissionSets: ['P'] on position P or to rename one of the two. It was added by #13419 slice 2, PR #13724.",
"consumers_through_the_one_resolver": "28 direct call sites of this.resolvePermissionSetsForContext in security-plugin.ts, plus the explain engine's resolveSets wiring (:2062, :5216). Among them: canReadObject :6273, canWriteObject :6549, canExport :6668, getReadFilter :5871 (RLS), resolveSharingReadFilter :5366, resolveWriteScopeForSharing :5457, the FLS projection mask :6086, getEffectiveObjectPermissions :6758 (the object map the current-user endpoint serves), explainAccessForCaller :5028/:5176, delegation narrowing :2198, describeDelegableScope :2304, and resolveOperationPrincipals :8591. The service method resolvePermissionSetNames :2255 also feeds the docs-audience gate (packages/rest/src/meta-item-read-gate.ts:648).",
"mirrors_outside_the_resolver": "delegated-admin-gate.ts:825-835 setsCarriedByPosition, own name at :833. It is read by the organization-level boundary (:802) and, since PR #24793, by the delegated walk setsBoundToPosition (:1447-1450); ownNameNote is at :274. explain-engine.ts:2247 attributes a resolved set whose name equals a held position as via position:NAME. packages/core/src/security/resolve-authz-context.ts:1642-1648 drops a deactivated position's NAME from positions (:1683), with the stated reason that 'a name left standing would resolve the same grant one layer down'.",
"not_consumers": "Approvals' position approver (plugin-approvals approval-service.ts:2101, holders via sys_user_position plus sys_member.role) and sharing-rule position recipients (plugin-sharing position-graph.ts) both expand a position to its HOLDERS. Neither reads a permission set.",
"half_applied_source_reading": "The core grant envelope carries only the sets a definition lists and the sets granted directly (positionBoundSetNames resolve-authz-context.ts:1150, foldCatalogSetBodies :1171). So an own-name set's systemPermissions and tabPermissions never reach context.systemPermissions or tabPermissions, and that envelope is what the capability checks read: rest-server.ts:1824, meta-item-read-gate.ts:752, sharing-rule-service.ts:582, current-user-endpoints.ts:1025. Net effect: the fold grants a same-named set's object, field and row half, and not its capability or tab half. In HotCRM this is latent: its 5 fold-reached sets carry no systemPermissions or tabPermissions (grep, 0 hits). This was read at source only and not run.",
"is_it_declared": "Not as a mechanism, in spec or in docs, and both say the opposite. Spec packages/spec/src/identity/position.zod.ts:64: 'capability reaches a position ONLY through the permission sets it names in
permissionSets'; :92: 'A holder of the position holds every set named here'. Docs content/docs/permissions/positions.mdx:10-13: 'The definition is the only binding … Positions carry no capability of their own'. The form row is packages/spec/src/identity/position.form.ts:34. The ADRs only acknowledge the fold as transitional. ADR-0090 D12 amendment (docs/adr/0090…md:564-567): 'while the resolver folds held position names into the requested set names (…), the permission set of the position's own name'. ADR-0138 D3 item 3 (0138…md:366-368): 'not the position-name fold that the ruling recorded on #13419 retires'. ADR-0131 D3/D4 (0131…md:291-296, 360-363) make the definition's permissionSets the binding and do not mention the fold. The code comment at security-plugin.ts:6820-6822 says the same: 'the position-name fold (#13419's ruling retires it)'. It is undeclared code."},
"2_what_setup_shows": {
"position_page": "objectui packages/app-shell/src/views/metadata-admin/previews/PositionPreview.tsx:61-72 renders a STATIC note: engine.positionPreview.noAccess, 'Positions don't grant data access on their own.', plus .bind (i18n.ts:1716-1718, zh :5292-5294). It reads no server state. The Permission Sets field is the spec form row position.form.ts:34 (ref-multi:permission), rendered over the registry definition's permissionSets, which is empty on all 12 HotCRM positions.",
"assigned_users": "AssignedUsersSection.tsx reads three things. Direct grants: sys_user_permission_set by permission_set (:242). The registry's position list (:252), filtered by positionsDistributing (:177-182), which keeps a position only when p.permissionSets.includes(setName). Holders of the kept positions from sys_user_position (:265). The own-name position never passes that filter, so its holders are absent. objectui#2383 (fixing objectui#2382, +216 −28, 4 files) added the position half for explicit bindings only.",
"why_absent_from_both": "Both panels read the declared binding, which is what spec and docs say the binding is. Only the server's undeclared fold grants more, and no server surface answers 'who effectively holds set S'. packages/rest/src/security-routes.ts serves only explain (:337/:343), my-delegable-scope (:395) and suggested-bindings (:479-540). The console's wording agrees with spec; it is the runtime that diverges."
},
"3_real_pull": {
"hotcrm": "f22dfa52: src/sales/sharing/positions.ts declares 12 positions, none with permissionSets (git grep, 0 hits). src/sales/profiles declares 8 sets: guest_portal, marketing_user, sales_manager, sales_rep, service_agent, service_manager, system_admin, tenant_admin. 5 positions rely on the fold: sales_manager, sales_rep, service_agent, service_manager, marketing_user. Explicit bindings: 0. DemoOrgStaffing staffs 4 of the 5. HotCRM writes the fold into its tests: test/demo-staffing.test.ts:292 ('A set whose NAME matches a position is bound to it at install time') and :357. It also writes it into its notes: demo-staffing.ts:134, :159, and service-agent.profile.ts:103-146, where the #1779 binding is a renamed spread, ServiceManagerProfile, because 'There is no authorable binding on either side'. The same note records that on 18.0.0-next.1 PositionSchema accepts permissionSets, and that 'Whether to declare the binding instead was not decided there' (:127).",
"could_it_use_explicit_bindings": "Yes. HotCRM is on @objectstack/* 18.0.0-next.5, where PositionSchema.permissionSets exists. Declaring permissionSets: ['P'] on the 5 positions changes nothing under today's fold: the set arrives through resolve-authz-context section 6b, and reportNameFoldCollisions and the delegated walk then count it as governed. It also makes Setup's two panels correct at once, with no platform change. The #1779 ruling ('bind the EXISTING service_agent set to the service_manager position') becomes directly expressible as permissionSets: ['service_agent']. Under the fold it can only be spelled as a renamed copy.",
"objectstack": "81120caac8. The 6 built-in positions and the 8 shipped sets share no name. examples/app-crm has sales_rep, sales_manager and finance_approver, and all three bind crm_sales_user explicitly (sales-positions.ts:16, :23, :36). examples/app-showcase has 10 positions: 8 bind explicitly (positions.ts:31-101), and finance and legal bind nothing. No example set name matches any position: 0 collisions. This agrees with #24736's census (19 positions, 19 sets, 0). The vendored HotCRM 17.1 artifact is test-only, and that is pinned by scripts/check-position-name-fold-loaders.mjs (.github/workflows/lint.yml:964-965).",
"not_measured": "Positions and sets authored at runtime in deployments. cloud's own packages (not in this container). Ruling 6259608309 on #15194, item 1, bounds the first: 18.0 is for fresh installs only, and every v17 database (cloud's hosted tenants and existing HotCRM databases included) stays on 17.x until the 18.x release that carries the upgrade path."
},
"4_13419": "Ruled 2026-08-31 (5472608110, the maintainer's 「同意」 to B): 「按名折叠通道退役」. Execution points: 1 census; 2 materialise (「⛔ 不物化不删折叠」); 3 「删折叠 + 发布/lint 门对「permission set 名与 position 名撞车且无 junction 行」响亮告警」; 4 implicit-grant audit; 5 「任何行为差异只能表现为拒绝/告警,⛔ 永不静默改变解析结果」. Delivered: slice 1, PR #13643 (the census); slice 2, PR #13724 (the fixture-collision determination, the loaders gate and the runtime warning). The deletion was parked as pm:on-hold (5480577256). Its Restart-when is a deployment-side reading through the census's --audit, or a maintainer ruling that the population outside the repo may be treated as zero. The card closed completed (5536457160, 'Answered by ADR-0131'): the junction retired, the binding is the definition's permissionSets, and 'the binding is written, not inferred'. That comment also says 'Name-based resolution is not a fallback; it is the mechanism'. It reads as ADR-0131 D4's by-name references, but it can be misread as declaring the fold. The deletion was never executed, and no open card carries it. The later records keep the retirement standing: ADR-0138 D3 item 3, the code comment at security-plugin.ts:6820-6822, and ADR-0090 D12's 'while'.",
"5_explain": "Yes, per principal. explain-engine.ts:2244-2250 viaOf reports via position:NAME for a set whose name equals a held position. objectui AccessExplainPanel.tsx renders it (:802, 'permission_set · via position:sales_manager'), and HotCRM's note records it live as via: position:service_agent. But the panel is mounted only in Studio (StudioDesignSurface.tsx:8831), not in Setup. It also answers 'why does user U hold this', never 'who holds set S', so it cannot fill Assigned Users. Side reading: the same viaOf labels a set reached through a position's permissionSets as 'direct grant'. That is because buildContextForUser (:688) takes context.permissions from resolveUserAuthzGrants, whose section 6b pushes those names (see out_of_scope_findings)."
},
"tests": "None run. This is a readings-only dispatch: no build closure, no tests and no runtime measurement. Every reading above is a source read at the named shas (git show and git grep against the object store, with no checkout edited), plus REST reads of #24757, #24736, #13419, the ruling records 6259608309, 6261447528 and 6279033298, and PR stats. The half-applied reading and the explain side reading are source-only, and are marked that way.",
"gates": "None. A readings-only dispatch derives and runs no gates and opens no PR (os-dev.md 何时停手不写码, status reading).",
"mcp_calls": "0 — no MCP GitHub tool was called. Every GitHub read was a gh api REST GET.",
"api_writes": "1 — this os-dev-report comment, POST /repos//issues/24757/comments, through scripts/pm/post-stamped.mjs (the fleet write path). No git push, no label and no assignee write.",
"open_questions": [
{
"question": "How is the position-to-same-named-permission-set grant (the fold, security-plugin.ts:6830) resolved, and is its retirement pulled into 18.0 (「是否提前」)?",
"options": [
"A — Make it visible. A1, objectui only (repo:objectui): positionsDistributing also keeps the own-name position (AssignedUsersSection.tsx:177-182), and PositionPreview reads the registry for a same-named set and says it applies. Precedent: objectui#2383, +216 −28, 4 files. No served surface, so no Clause-②. A2, server-served effective holders: a new route in packages/rest/src/security-routes.ts plus a fold-aware plugin-security method that objectui consumes. That is a new served API surface (a Clause-② widening) spanning domain:services, rest and objectui, with no precedent. Both keep the fold and add another copy of an undeclared rule: after the resolver :6830, the delegated walk :833 and explain :2247, the console becomes the fourth. Both put on screen what spec position.zod.ts:64 and positions.mdx:10-13 deny. Both leave the fold half-applied, and both are reverted if C lands.",
"B — Materialise. The platform cannot write into a sealed managed definition (ADR-0131 D6), so for code-declared positions B means the package declares permissionSets: ['P']. In HotCRM (repo:hotcrm) that is 5 entries in positions.ts, the test predicates at demo-staffing.test.ts:292 and :357, and the notes. Today that is behaviour-neutral, and it fixes the measured install's Setup at once. For 17.x databases, the pure conversion packages/core/src/security/position-binding-conversion.ts gains an own-name pair rule. The upgrade ceremony applies it (packages/cli/src/utils/organization-ownership-apply.ts, domain:cli). Materialising at boot is excluded by that file's header and by ADR-0131 D10 ('Never at boot'). 18.0 ships no ceremony (6259608309), so this half is 18.x. On its own, B leaves the fold for everything not materialised: it is C's migration, not an alternative.",
"C — Retire the fold (#13419's standing ruling). objectstack, domain:services: security-plugin.ts :6827-6830 requests explicitPermissionSets only. Delete dropDeactivatedFoldedSets (:6917-6934). Re-point reportNameFoldCollisions (:6987-7026) to the loud half: a held position P whose catalog has a set P that its definition does not list warns 'not granted; declare permissionSets'. A boot-time pass over the registry is the cheaper place for that check. Also delegated-admin-gate.ts :833/:274 (ADR-0090 D12's 'while' clause lapses on its own wording), explain-engine.ts :2247, and the comment at resolve-authz-context.ts :1642-1648 (the drop at :1683 stays). Tests to retire or rewrite: position-name-fold-warning.test.ts (244 lines); delegated-admin-own-name-set.posture.test.ts (228, from PR #24793); the fold-preservation pins in orgless-position-name-fold.test.ts (341); and the own-name cases in delegated-admin-gate.test.ts and guest-grants-channel.test.ts. One gate retires: check-position-name-fold-loaders.mjs (805 lines) with lint.yml:964-965. The system-context.mdx anchors are refreshed with that gate's own --fix. The lines net negative. There is no precedent for the deletion itself. For scale: two PRs on the fold's own mechanisms were #24793 (+429 −19) and #13724 (+859 −7), and an 18.0-pulled retirement, #24721, was +944 −37 across 24 files. Prerequisite, ruling point 2: HotCRM's 5 declarations land first. Once both land, #24757 closes with zero objectui change. The panel's existing position half (#2383) and the form's Permission Sets field then show the binding, and 'Positions don't grant data access on their own' becomes true. It is a narrowing behaviour change, so 「是否提前」 applies.",
"D — Text only (objectui copy, i18n.ts:1716-1718, zh :5292-5294, plus the Assigned Users empty copy). Effort XS. But today's text is the spec's truth. Rewriting it to describe the fold declares on screen what spec and docs deny, which inverts spec over implementation over docs. And Assigned Users still shows 0 for a set that carries viewAllRecords, so the card's audit gap stays."
],
"recommendation": "C, pulled into 18.0 and sequenced HotCRM-first (B's package half), with the existing runtime warning re-pointed as the loud half. The ceremony's own-name rule ships with the 18.x ceremony. Not A, not D. Argued on the four axes. 实际业务需求: the measured pull for name-matching is 1 app (HotCRM, 5 of 12 positions), with 0 in the examples and 0 in the built-ins. HotCRM needed a binding when none was authorable (its own note), and permissionSets now meets that need with 5 entries. Nothing measured pulls for an effective-holders API beyond this card. 项目长远合理性: spec and docs declare the definition as the only binding; ADR-0131 D3/D4, the #13419 ruling and ADR-0138 D3 all point at retirement. The fold is undeclared code (spec over implementation). It is half-applied: object and row access yes, capabilities and tabs no. It has already needed five compensating mechanisms and a CI gate, and it carried one cross-organization defect (#20555). A builds a fourth copy that C would later revert, and D writes the divergence down as copy. 防 AI 写错: a grant by naming coincidence is the shape an AI writes without seeing it. HotCRM's own notes teach 'declare X's grants under the name P'. C makes declared equal enforced and visible in Setup, while A and D bless an implicit channel at the consumer. The AI error that remains after C is writing a set with the position's name and no permissionSets entry, and the re-pointed warning keeps that loud. An author-time lint would be a new gate. I do not recommend one unless the maintainer re-affirms the 「发布/lint 门」 wording of ruling point 3, which was written for the now-retired junction. 创业阶段不扩散: C deletes code, tests and one gate. A2 adds a served surface. Retire immediately, with no staged window: the one named consumer (HotCRM) switches with 5 entries inside the 18.0 prerelease line. The 「是否提前」 line. Kind: narrowing. If not pulled forward, then after 18.0 GA it waits for 19.0, or ships only as the warning that already ships. 18.0 is for fresh installs only, so no existing database reaches it, and pulling it now meets the smallest population it will ever have: HotCRM's 5 code-declared positions. Workload: HotCRM S (5 entries, 2 test predicates and notes); objectstack M, net-negative lines. No PR is open. Lean: pull forward. Two points are the maintainer's to confirm. First, that 18.0's fresh-install scope satisfies #13419's Restart-when (the population outside the repo treated as zero for 18.0). Second, that the 2026-09-04 closing comment (5536457160) did not re-declare the fold. If he declines to pull it forward, the interim is B's HotCRM half alone: it fixes the measured install's Setup while the fold and its warning remain. Not A or D."
}
],
"out_of_scope_findings": [
"class: a candidate (a misleading user-visible attribution) · reach: NOT MEASURED. This readings-only dispatch booted no stack, so it carries no public-door reading and is not fileable as-is · evidence: explain-engine.ts:2244-2250 viaOf checks positions, then context.permissions. buildContextForUser (:688) takes context.permissions from resolveUserAuthzGrants, whose section 6b pushes every set a position's permissionSets lists, so the explain report labels such a set 'direct grant'. The Studio AccessExplainPanel renders that label (:802). If C is chosen, the same function is edited (:2247) · dedupe words: explain via direct grant, position-bound set attribution, viaOf, AccessExplainPanel contributors · carrier: 承接者:无 — noted, not filed"
],
"deviations": [
"The shared checkout's origin/main moved from 81120ca to 6b9e6df mid-reading, after a sibling's fetch (refs/remotes is shared). The cited files are unchanged across that commit, re-checked by symbol at 81120ca. No checkout was edited and no fetch was made.",
"HotCRM was read at the card's f22dfa52 and not at the local origin/main: the local checkout is shallow and stale (b94cf856). A REST compare to remote main 9db9a58b shows no change to the security-bearing source."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
[Decision] 职位按同名折叠授予权限集:按 #13419 已裁的退役,是否提前进 18.0?
domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T12:24Z。依据:本卡只读读数报告6290658622(origin/main81120caac8,objectui pin616df6598,hotcrmf22dfa52);席位复核读数在 origin/main6b9e6df909。Release:
session_01UmqzD97e14iws6hmVhgZ5y· 因:只读读数已交付;方向已由 #13419 裁决 B 与 ADR-0131 定下,余下只有时机(「是否提前」规则6261447528,以及 #13419 删除半边停放时写下的Restart-when:)· 去向:needs-user-decision,本笔清 assignee。一句话问题: HotCRM 有 5 个职位(销售经理、销售代表、客服专员、客服经理、市场专员)靠「职位名 = 权限集名」的隐式折叠拿到数据权限。Setup 的职位页写着「职位本身不授予任何权限」,权限集的「已分配用户」也是空的,可这 5 个职位实际授予了对象、字段与行级权限。
背景(实测)
security-plugin.ts:6830const requested = [...positions, ...explicitPermissionSets],把持有的职位名直接当权限集名请求。spec 与文档都没有声明它,而且写的是反面:position.zod.ts:64「能力只经permissionSets点名的权限集到达职位」;positions.mdx:10-13「定义是唯一绑定」。systemPermissions)与标签页那一半走核心授予信封,信封只认定义里声明的集。HotCRM 的 5 个集都不带这两项,所以目前是潜在问题。permissionSets。5472608110):「按名折叠通道退役」。普查与碰撞告警已交付(PR Census the position-name fold and generate the implicit-grant audit report (#13419 slice 1) #13643、Determine the #13419 name-folds are a fixture collision, and warn loudly on ungoverned same-name grants #13724)。删除半边停放为pm:on-hold(5480577256),其Restart-when:是部署侧普查读数,或维护者裁定「仓外人口可视为零」。卡随 ADR-0131 关闭(5536457160:「the binding is written, not inferred」),此后没有卡承载这次删除。6259608309):v17 库不升 18.0。Governing text
packages/spec/src/identity/position.zod.ts:64、:92;content/docs/permissions/positions.mdx:10-13。permissionSets上,按机器名引用。5472608110)的执行要点,原文照引:要点 2「⛔ 不物化不删折叠」;要点 5「任何行为差异只能表现为拒绝/告警,⛔ 永不静默改变解析结果」。6261447528(维护者原话「后续的任务如果有涉及到spec元数据协议相关的修改,也可以和我沟通是否提前」);分诊6283779151把本卡归入此规则。是否改协议: 不改 spec,文档已经写的就是目标状态。ADR-0090 D12 的「while」条款按其自身措辞随之失效,不需要改 ADR 正文。这是运行时授予的收窄。
不在决策范围内的选项: 「在控制台把折叠显示出来」(A,objectui 或新服务接口)和「只改文案」(D)都与上列 spec、ADR-0131 和 #13419 裁决冲突:它们把 spec 否认的通道搬上屏幕。读数报告已逐项评估,席位不再作为选项呈报。
前提(复核命令)
git grep -n "const requested = \[...positions, ...explicitPermissionSets\]" origin/main -- packages/plugins/plugin-security/src/security-plugin.ts(应 1 处)。git grep -c permissionSets f22dfa52 -- src/sales/sharing/positions.ts(应无匹配);对照git show f22dfa52:src/sales/sharing/positions.ts | grep -c "name: '"(应 12)。5480577256之后 Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 上无落地评论)。无论怎么裁都先做的一步(已立卡)
objectstack-ai/hotcrm#2200:HotCRM 在 5 个职位上声明
permissionSets。这是 #13419 要点 2「物化」在 HotCRM 的那一半。今天它不改变任何行为:折叠与声明请求的是同一个集,5 个集也都不带能力和标签页那一半。HotCRM 的 Setup 立即显示正确,本卡的实测症状随之消失。选项(只有时机)
:6830的职位名请求,删dropDeactivatedFoldedSets,把现有运行时告警改成「同名集未声明 ⇒ 未授予,请声明permissionSets」,同步收拾委派遍历、explain 与碰撞门禁业务含义直译: 「提前」= 趁 18.0 只有新装用户,把「起同名就自动授权」这条暗道一次拆掉;「不提前」= 先把 HotCRM 改成明写,暗道留到下个大版本。
推荐:提前(HotCRM 先落,objectstack 后删)
permissionSets。Setup、explain、委派遍历读同一份声明,不再需要五套补偿机制和一个 CI 门禁来盯一条没人声明的通道。参照 ADR-0131 D3/D4 与 Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 裁决 B。6259608309),此刻受影响人口最小,仓内实测只有 HotCRM 的 5 个代码声明职位,hotcrm#2200 一次付清。正式版发布后,这次收窄要么等 19.0,要么只能发告警。Restart-when:裁定「仓外人口可视为零」。对这部分应用,要点 5 由告警兑现:行为差异只表现为响亮告警加拒绝,不是静默。explain-engine.ts:2244-2250)。可达性未实测,按立卡门不立卡,记在此处;若裁「提前」,同一函数会在退役 PR 中被改动。os-decision-facets
Prior rulings read: position-name fold,name fold,permissionsets,sys_position_permission_set → 4 hits; ADR-0131 D3, ADR-0090 D8, ADR-0094 D1, ADR-0131 D13; thread: none; repo: objectstack-ai/objectstack
(Cross-checked by
git grep -l -iE "position[- ]name fold|name[- ]fold|按名折叠" origin/main -- AGENTS.md docs/adr packages/spec/src: 1 file, ADR-0138. ADR-0090 D12 and ADR-0131 D3/D4 were read directly. Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419's ruling5472608110and closing5536457160are outside this corpus and are quoted above.)裁后执行
target:v18,转pm:blocked+Blocked-by: objectstack-ai/hotcrm#2200。hotcrm#2200 合并后,席位派发退役:删折叠、改告警、同步委派遍历、explain 与碰撞门禁。门禁接线在.github/workflows/lint.yml,属 Tier H,那个 PR 留 draft 等你批准。Setup 不需要改 objectui。Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 的停放就此由你的裁定解除,席位在 Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 上留一条指针评论。pm:on-hold,Restart-when:写 19.0 开线。5536457160的「Name-based resolution is not a fallback; it is the mechanism」是在声明折叠本身(席位读作 D4 的按名引用,与同段「the binding is written, not inferred」一致),请直接说明,席位据此重开方向问题。Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Maintainer ruling 「甲」 recorded (#6015
6292005854). 18.0.0 has shipped, so the question is now 18.x versus 19.0. Triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T13:32Z6290658622), the platform cannot write into a sealed managed definition (ADR-0131 D6). So no conversion can carry the retirement of the same-name implicit grant.permissionSetson them hotcrm#2200 (HotCRM declarespermissionSetson its five positions), and the existing runtime warning re-pointed as the loud half.needs-user-decision→pm:queue.domain:servicescarries out the 18.x half.Generated by Claude Code
6 remaining items
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24757,
"status": "done",
"branch": "claude/issue-24757-fold-warning-18x",
"pr": "#24988",
"session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching seat's id (mode:subagent, shared)",
"premise_still_valid": true,
"summary": "18.x half of #24757 under 「甲」: the position_name_fold_grant warning in plugin-security is re-pointed as the loud half of the fold's retirement. Its text now says the set is in force for holders of position P only because the two share a name, that this name-based grant is deprecated and removed in the next major version (after which holders of P do not receive set P unless P's definition names it), and the fix (permissionSets: ['P'] on P, or a rename). The structured payload gains removedIn: 'next_major', and event/position/permissionSet are unchanged. Trigger predicate, once-per-position cadence and every resolution result are unchanged; the fold, dropDeactivatedFoldedSets and the 19.0 follow-ups are untouched. PR #24988 is a draft opened with Part of #24757. Every PM mechanism assumption held: the warning is lazy (first resolution meeting an undeclared pair, never at boot), fires once per position NAME per plugin instance (not per holder, not per organization), shipped in 17.3.0 (CHANGELOG changeset 936aa2d), and nothing but the log line consumes its payload. #24962 had not landed when the PR opened.",
"tests": "plugin-security position-name-fold-warning.test.ts: 29 passed (28 before; the new case is the exact-payload-keys pin), via pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/position-name-fold-warning.test.ts, 'Test Files 1 passed (1) · Tests 29 passed (29)'. Full suite at 63246ce: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2, 'Test Files 197 passed (197) · Tests 4078 passed | 45 skipped (4123)', lock VERDICT command-exit 0. Typecheck at 63246ce: pnpm --filter @objectstack/plugin-security run typecheck (tsc, tsconfig.scripts.json, check:test-typecheck) VERDICT command-exit 0, 'check:test-typecheck: OK — 0 file(s) / 0 error(s) / 0 pinned signature(s)'. The test file is in the tsconfig.test.json program (--listFilesOnly count 1) and not in tsconfig.json's (0). Ablations ran through scripts/ablation-replace.mjs in WRAP mode, each leg 'ok mutation landed' then 'ok restored: blob == HEAD (5db0a1f3c00a) and git diff HEAD is empty', and the outer script re-checked blob and diff bytes = 0. The tests import ./security-plugin.js from src, so there is no dist leg. Child exit 1 on every leg: A1 drop removedIn → 4 red (both MUST FIRE, payload keys, once); A2 put back the 17.x 'in force and ungoverned' wording → 1 red (text); A3 tracker id in the string → 1 red (text); A4 explicit sets not treated as declared → 1 red (declared pair silence); A5 baseline not treated as declared → 1 red (baseline silence); A6 dedupe removed → 1 red (once per position); A7 predicate 'bound to anything?' → 2 red (sales_rep and sales_manager bound to crm_sales_user); A8 position-name request removed (the 19.0 removal early) → 5 red; A9 warning kept but the undeclared same-name set dropped after it → 4 red, each on the grant assertion ('expected [ crm_sales_user, member_default ] to deeply equal [ …(3) ]', 'expected [ member_default ] to include sales_rep'). A9's first attempt was a no-op: the replacement contained the anchor, the tool refused ('the anchor count moved 1 -> 1') and restored, and the leg was re-run with a non-containing replacement.",
"gates": "Local, at e41097d (the PR head), from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack. 69 commands, the same 69 as the PM reading at 6440cf1. Each exit was captured before any pipe. 67 exit 0. 2 exit 3 PREREQUISITE NOT MET, NOT MEASURED: pnpm check:dual-build-cjs-loads and pnpm check:i18n need every package's dist/, and only plugin-security and its closure were built; CI runs both. Reconciliation: --ran with the exit-coded record printed 'dispatch-gates --ran: 69 derived famil(ies) accounted for — 67 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)'. The roster gates whose roster sits beside this diff were also run: check-changeset-fixed.mjs, check:authz-resolver, check:error-code-casing and check:filter-alias-parity, all exit 0. check-partof-closing-keyword was run over the PR body with PR_BODY set: exit 0. Build: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build, then plugin-security build, both VERDICT command-exit 0. CI check-runs on e41097d at report time: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped. in_progress: Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped ×3, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter. Test Core and Build Core had not started. Left to CI: pnpm lint and the farm, test:repo, the type-check lanes, Test Core and Build Core.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 REST writes, executed by the fleet relay as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft) → #24988; POST /repos//issues/24988/assignees [os-support-ai]; POST /repos//issues/24757/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). The relay was reached by 2 POST /repos/objectstack-ai/objectstack/dispatches under the session token. Also 3 git pushes, which are not REST: the empty-branch write probe at b811c73, 63246ce and e41097d. No label, card-assignee or body PATCH writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #24757 (its 19.0 half, which removes this warning) · noted, not filed · observation, source reading only, no public-door repro: nameFoldWarned keys on the position name alone and the payload names no organization, so on a multi-organization deployment an undeclared pair P in a second organization is not reported until restart once another organization's P has warned · dedupe words: name fold warning dedupe organization, nameFoldWarned per name, position_name_fold_grant once",
"carrier: #24757 (19.0 half) · noted, not filed · observation: the warning is lazy, so a deployment where no holder of P has made a request since boot has not been told; a boot-time registry pass would be a new trigger, outside this dispatch's keep-the-trigger instruction · dedupe words: fold warning boot time, lazy warning holder request",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only · the nameFoldWarned field docblock links {@link reportNameFoldCollision} (singular), which names no member; it is outside the claimed regions of security-plugin.ts, so it was not touched"
],
"files_changed": [
".changeset/24757-fold-warning-retirement.md (+13 −0, '@objectstack/plugin-security': patch, Clause-②: no; the package is not private)",
"packages/plugins/plugin-security/src/position-name-fold-warning.test.ts (+81 −31)",
"packages/plugins/plugin-security/src/security-plugin.ts (+76 −22). Hunks at 905-919 (the POSITION_NAME_FOLD_EVENT docblock) and 7008-7131 (the reportNameFoldCollisions docblock and body) only. Nothing near computeControlledByParentFilter or its call sites (#24962's region)."
],
"line_budget": "223 changed lines (+170 −53) over 3 files vs ba69b16, under the 3000 human-merge threshold. No governed surface. No skills/** file, so no skills line ratchet applies.",
"deviations": [
"Commit trailers: the model-free pair AGENTS.md prescribes (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the reminder defers to the repo's own rule.",
"The suite and typecheck ran at 63246ce. The branch then merged origin/main ba69b16 (one commit, plugin-approvals only, outside plugin-security's dependencies and devDependencies), and neither was re-run at e41097d. Gates ran at e41097d.",
"origin/main moved to 78ab647 after that merge (#24959, #24980, #24976: service-settings, cloud-connection, rest, service-messaging, plugin-auth and platform-objects setup-nav.contributions.ts; nothing in plugin-security). It was not merged before pr_create; CI's merge ref carries it. #24962 had not landed: its branch claude/issue-24962-cbp-master-object-read was at 438ef2d with no PR when checked.",
"Provenance: the checkout is shallow, so git log -S could not reach PR #13724. The warning's introduction is cited from the package CHANGELOG's 17.3.0 entry (changeset 936aa2d) instead.",
"test:repo was not run locally (it is CI's). check:dual-build-cjs-loads and check:i18n are NOT MEASURED locally (exit 3, prerequisite), as stated in gates.",
"Explain line: 「甲」 lists it among the loud-half forms. Triage's derivation for this card names only the warning, and explain-engine.ts is fenced, so no explain text changed.",
"Cleanup: node_modules and the worktree /home/user/objectstack-issue-24757 were removed after pr_create (git worktree remove exit 0, no --force). Every process this run started had exited (install, suite and gate PIDs were gone). No dev server was started."
],
"pr_body_edits": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24988 comment 6303114132 (the 18.x half) ·
domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T22:23Z · heade41097d233, ordinary queue landing. The card stays open for its 19.0 half.objectstack-fleet commentedon Oct 20, 2026
Landed (18.x half), and
pm:dispatched→pm:on-hold·domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T22:56Zf90ea677c.git merge-base --is-ancestor f90ea677c origin/mainholds. The PR wasPart of #24757, so the card stays open.position_name_fold_grantwarning now states that the same-name grant is removed in the next major version, what holders lose then, and the fix (permissionSets: ['P'], or a rename). The payload gainsremovedIn: 'next_major'. The grant itself is unchanged in 18.x. ACCEPT6303114132.6292005854; triage's derivation for this card6292072572), the fold's removal waits for 19.0.Restart-when:, the version inpackages/spec/package.jsononorigin/mainstarting with19..Release:
session_01UmqzD97e14iws6hmVhgZ5y· 因:18.x 半边已落地,余下 19.0 半边按「甲」停放 · 去向:pm:on-hold,本笔清 assignee;19.0 开线时由车道席位重新认领。objectstack-fleet commentedon Oct 21, 2026
pm:on-hold→pm:blocked: the retirement ships in 18.x, after HotCRM declares ·domain:servicesseat 2 (seat post #21118) ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T00:18ZThe maintainer amended 「甲」 (
6303865025on #6015): a narrowing that no conversion carries now also ships in 18.x. #6015's record names this card among those its seat re-reads. Re-read:Restart-when:line is removed.6290781571, 裁后执行 for 提前): HotCRM declarespermissionSetson its five positions first, so the retirement revokes nothing. The body now carriesBlocked-by: objectstack-ai/hotcrm#2200.dropDeactivatedFoldedSetsremoved, the warning re-pointed, and the delegation walk, explain and the collision gate following. The gate wiring (.github/workflows/lint.yml) makes that PR Tier H, a draft for the maintainer's approval.objectstack-fleet commentedon Oct 21, 2026
Unlock:
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-21T10:05Z. ⛔ Not a claim.Blocked-by: objectstack-ai/hotcrm#2200: HotCRM's declaration closedcompletedat 2026-10-21T06:30Z. Per the seat's own note of 2026-10-21T00:18Z, thedomain:servicesseat now dispatches the retirement. Its gate wiring makes that PR Tier H: draft, with both approvers requested.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
pm:queue→pm:on-hold: HotCRM's declaration is merged but not yet released, so the retirement would still revoke ·domain:servicesseat 2 (seat post #21118) ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T14:49Z · Thread-read: 6313352208Source: the seat's grading under the cross-repo unlock rule. A cross-repo unlock is judged on what a consumer can install, not on an upstream merge. It is not a maintainer ruling; the direction (the 18.x retirement,
6290781571with 「甲」 as amended,6303865025) is unchanged. The triage unlock6313352208read "hotcrm#2200 closed", which holds as a GitHub fact. This comment adds the install-side reading it did not take.Measured (HotCRM checkout, this stamp):
mainc8a7a704(better-sqlite3 → wasm auto-fallback doesn't cover the persistent-file / --artifact dev path (only the zero-config :memory: branch) #2229, which closed hotcrm#2200) declarespermissionSetson its positions:git grep -c permissionSets origin/main -- src/sales/sharing/positions.ts→ 6.v5.1.2(0a8f73d9, 2026-10-19), declares none (→ 0). The same holds forv5.1.1andv5.1.0. There are 33 unreleased commits on HotCRMmain.v5.1.2pins@objectstack/*exactly (18.0.0-next.3).main= framework9d13b5fc, HotCRM 5.0.0). If the retirement landed now, the cloud's next framework pin move would strip the five same-name positions' grants from a released HotCRM. That is exactly what the order "HotCRM declares first, so the retirement revokes nothing" exists to prevent.Restart-when: git -C hotcrm fetch -q --tags origin && git -C hotcrm grep -q permissionSets "$(git -C hotcrm describe --tags --abbrev=0 origin/main)" -- src/sales/sharing/positions.ts
The dispatch this hold wakes into (pre-written):
6290781571's execution section:const requested = [...positions, ...explicitPermissionSets]) anddropDeactivatedFoldedSets;permissionSets";.github/workflows/lint.ymlmakes the PR a draft that waits for the maintainer's approval.AGENTS.mdas it reads at dispatch. The maintainer's ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105 (PR feat(gates): the changeset level follows the metadata protocol —Metadata-protocol:line, level axis, console refresh at patch, release notes by reader action (ruling A) #25114) now ships a non-protocol change aspatchwith its BREAKING banner.6290781571: explain labels a position-bound set as "direct grant" (explain-engine.ts, re-locate onmain).