You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
app-crm: declare the Pipeline dashboard's audience gate (requiredPermissions on pipeline_dashboard and nav_pipeline_dashboard), so a member who cannot read its data is not offered it #27735
Filing gate: ② a wrong state offered to the user. Filed by the seat on the maintainer's ruling 6508997538 on #27491, item 5: 「C runs beside A, not instead: domain:ui#2 files one S card in the app lane」. ⛔ Not paired with #27491's PR.
Business reason
Who: a CRM member with no sales permission set. What happens today: the CRM landing says "Nothing in CRM is available to you yet", but the sidebar still offers Analytics → Pipeline. Clicking it opens a dashboard every tile of which the server refuses (all six widgets read opportunity_metrics). Once #27491 lands, that member gets a no-access panel instead of a blank canvas. The app still offers them a page they cannot use.
Mechanism (read at origin/main)
examples/app-crm/src/dashboards/pipeline.dashboard.ts: pipeline_dashboard declares no requiredPermissions.
navObjectReadGate.ts prunes only type: 'object' nav entries, which is why the landing and the sidebar disagree.
⚠ The CRM permission sets (examples/app-crm/src/security/sales-positions.ts: sales_rep, sales_manager, finance_approver, crm_sales_user) declare object bits only, with no capability. So the gate needs a capability that a sales set actually GRANTS. The implementer either adds one to the set(s) that read opportunity_metrics's object, or names an existing grant. A gate nobody holds would hide Pipeline from the sales users too.
Acceptance
pipeline_dashboard and nav_pipeline_dashboard declare the same requiredPermissions, with a capability that the CRM sales set(s) able to read the dashboard's dataset actually grant.
A plain member (no sales set): the CRM sidebar has no Pipeline entry, and a direct link to the dashboard gets dashboard-access-denied.
A sales user: sees Pipeline and its tiles as today.
Pins in the app's tests (examples/app-crm/test/**, beside the smoke test). Both rows are checked through the server's read gate, not a client guess.
Reader who acts
The lane that owns examples/app-crm (triage names it; app lane). Size S.
reach
Public door: CRM example → sidebar → Analytics → Pipeline, as any member without a sales permission set (the 18.3.0 RC walkthrough's member, #27377 W3-UX).
Dedupe
No existing card asks for the CRM dashboard's audience gate. I read the #27491 ruling 6508997538 (it names this card as not yet filed) and the 100 most recent open domain:ui / app issues by title for requiredPermissions, pipeline_dashboard and audience.
pipeline_dashboard and nav_pipeline_dashboard declare the same requiredPermissions, naming a capability that the CRM sales sets which read opportunity_metrics's object actually grant. Pin both: a plain member is not offered Pipeline, and a sales user still is.
⛔ The gate is never one that nobody holds. A capability added to an example permission set is example metadata, not a protocol key.
Claim: PM loop round 3 · 2026-10-29T23:15Z
Session: session_01Utp6CY2n1JmZZKrHgtp9jC (seat; implementation by a card thread, mode:thread)
Account: os-bill
Branch: claude/project-thread-ch022k (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
Worktree: objectstack-issue-27735
Domain: domain:ui
Seat: domain:ui#2
File surface: examples/app-crm/src/dashboards/pipeline.dashboard.ts, examples/app-crm/src/apps/crm.app.ts, examples/app-crm/src/security/** (one capability, granted in crm_sales_user), examples/app-crm/test/**, and — widened by the seat on the dev's stop-and-report — examples/app-crm/objectstack.config.ts (one capabilities: [...] line plus its import, so the capability is DECLARED on the stack: delegated-admin-gate.ts condition (c) refuses an undeclared systemPermissions name); no changeset (the example is private: true; skip-changeset); ⛔ package.json (no new devDependency); ⛔ packages/spec/**, packages/rest/**, every ui/** file
Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: no (expected; the dev measures from the diff)
Metadata-protocol: none (expected; the dev measures from the diff)
Responsibility: the CRM example declaring no audience gate on its Pipeline dashboard and nav item | the server's requiredPermissions read gate (ADR-0066) and the nav guard are fixed inputs | every CRM member without a sales permission set who is offered Pipeline today
Thread-read: 6509200885
Premise: holds as filed (the seat read origin/main while filing: neither declaration carries requiredPermissions, and crm_sales_user grants no capability)
Serial constraints cleared: no open PR touches examples/app-crm/** (seat read of open PRs' files); the renderer half is PR #27777 (#27491), not paired; seats 3 and 4 agreed to the split
Dispatch order: /mnt/project-files/ui-seat2/dispatch-27735.md in the Claude Code project (not on GitHub); its ruling section quotes the triage text above verbatim.
{
"issue": 27735,
"status": "done",
"branch": "claude/project-thread-ch022k",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27805",
"session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
"premise_still_valid": true,
"summary": "Added crm.view_pipeline (defineCapability, scope org, packageId com.example.crm) on the CRM stack's capabilities, granted it in crm_sales_user (systemPermissions), and declared requiredPermissions ['crm.view_pipeline'] on pipeline_dashboard and nav_pipeline_dashboard. Readers: createMetaItemReadGate dashboard arm (audience-permission 403) and filterAppForUserWithReason nav pruning, both in packages/rest/src/meta-item-read-gate.ts, against the union of the caller's sets' systemPermissions. The declaration (not only the grant) is required: plugin-security's organization boundary condition (c) refuses a set carrying an undeclared or platform-scoped capability. objectstack.config.ts added to the surface by the seat (claim amended). Named gap: the example has no @objectstack/rest dependency, so tests pin every input the gate reads, not the gate run (seat declined the devDependency).",
"tests": "pnpm --filter @objectstack/example-crm test: exit 0, Test Files 7/7, Tests 50/50 (new test/pipeline-audience.test.ts, 4 cases); typecheck exit 0; objectstack validate exit 0. Ablation (no build/dist path: example reads its own src): scripts/ablation-replace.mjs deleting the crm_sales_user systemPermissions line, anchor hit 1, holders case red (1 failed | 3 passed), restored blob == HEAD 314586f62842, git diff HEAD empty.",
"gates": "local at 87e10674: dispatch-gates --ran: 39 derived, 38 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, needs every package dist; CI's). check:i18n-coverage first exit 3 (missing connector-slack dist), rerun after its named prerequisite build: exit 0 OK none new. node scripts/check-sdui-manifest.mjs exit 0. CI check-runs: in_progress at report time.",
"line_budget": "7 files, +~150 (one new test file ~80, new capabilities.ts ~35, declarations ~15)",
"files_changed": [
"examples/app-crm/objectstack.config.ts",
"examples/app-crm/src/apps/crm.app.ts",
"examples/app-crm/src/dashboards/pipeline.dashboard.ts",
"examples/app-crm/src/security/capabilities.ts",
"examples/app-crm/src/security/index.ts",
"examples/app-crm/src/security/sales-positions.ts",
"examples/app-crm/test/pipeline-audience.test.ts"
],
"deviations": [
"objectstack.config.ts outside the dispatched surface: stopped and asked; seat granted and amended claim 6510769461 before PR",
"PR attribution link names this card thread (session-context requires it verbatim), not the seat thread link the dispatch spelled",
"first branch push (WIP) went out before the seat's surface answer; it carried the config line",
"no changeset: @objectstack/example-crm is private; skip-changeset label on pr_create (read back present)",
"harness post-PR assign/review step skipped per dispatch; PR has no assignee and no reviewer"
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 — relay pr_create (draft, labels skip-changeset) run 40976175747, body read back identical 7309 bytes; this os-dev-report comment. Plus git pushes (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"none filed. Noted: packages/spec/src/ui/dashboard.zod.ts JSDoc near requiredPermissions still says not enforced (already recorded by the #27491 review thread); carrier: #27491 ruling thread · noted, not filed"
]
}
Ruling: the CRM Pipeline entry's audience gate, after the measured cost of a declared capability (project discussion thread 「决定仪表盘全部无权限时的显示」, follow-up to #27491 ruling 6508997538 item 5) · letter G · maintainer 「同意」 2026-10-30T01:36Z
The maintainer's own ruling, given in the project thread at 2026-10-30T01:34Z on the review thread's six-section brief and its post-measurement addendum (/mnt/project-files/decisions/27735-种子管理员失去Pipeline-呈批.md, recommendation G). Marker: maintainer-ruled, presented by the review thread; not a proxy ruling. It amends the MECHANISM of #27491 ruling 6508997538 item 5; that item's intent (a member who cannot read the Pipeline's data is not offered it) stands.
What was measured and re-verified
PR fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities #27805 at 87e10674 (declared capability crm.view_pipeline, granted to crm_sales_user, required on pipeline_dashboard and nav_pipeline_dashboard). The card thread ran packages/qa/dogfood/test/install-local-unmanaged-mode.dogfood.test.ts against it: 11 failed / 9 passed; the control (the CRM at the merge-base, same build) 20/20. PR CI was green only because the dogfood shards were skipped on the PR.
10 failures: a stack-level capabilities declaration makes the CRM package refuse an UNMANAGED install, 422 UNMANAGED_INSTALL_UNCOPYABLE. By design: the capability metadata type is allowRuntimeCreate: false (packages/spec/src/kernel/metadata-plugin.zod.ts L1242, ADR-0131 D3 / ADR-0066 D1: a capability lives in the registry and is never an environment row), and the unmanaged import refuses whole any body carrying a runtime-uncreatable type (packages/metadata-protocol/src/package-unmanaged-install.ts L33). The dogfood uses the CRM as its unmanaged-install specimen. A package that declares capabilities cannot be installed unmanaged; this card does not change that.
1 failure (L460): the seeded admin's GET /meta/dashboard/pipeline_dashboard answers 403 audience-permission. holdsRequiredPermissions (packages/rest/src/meta-item-read-gate.ts L753) has no administrator bypass; the dashboard arm (L1690–1700) judges the audience gate before policy.arms, author or not, so the gate also closes the dashboard in Studio for a caller who lacks the capability. The seeded admin holds ADMIN_FULL_ACCESS_CAPABILITIES (eval-user.zod.ts L115): every platform capability, no app-declared one (system/dev-login.zod.ts L14–27 documents this as by construction).
Granting without declaring is not a way out: packages/plugins/plugin-security/src/delegated-admin-gate.ts L875–906, organization boundary condition (c): a systemPermissions name that is not a capability declared scope: 'org' puts the set outside the boundary, so an organization administrator could no longer assign crm_sales_user or the three sales positions that distribute it (ADR-0090 D12).
The nav guard already binds current_user.can(object, verb) from the member's permissions payload (objectui#4421; ui/packages/app-shell/src/hooks/useNavMemberGates.ts, ExpressionProvider.tsx), and BaseNavItemSchema.visible is a CEL predicate. opportunity_metrics is bound to crm_opportunity (examples/app-crm/src/datasets/opportunity.dataset.ts L17), the object whose read refusal is the six tiles' 403. visible is not judged by the server's read gate (meta-item-read-gate.ts L918), so the dogfood's admin reads are untouched.
Governing text
ADR-0066 D4: "visibleWhen hides, requiredPermissions refuses." This card's need is HIDE (the entry is not offered); the enforcement is the dataset query's 403, already in place and unchanged.
ADR-0131 D3 / ADR-0066 D1 (a capability is a registry declaration) and the unmanaged-install rule (package-unmanaged-install.ts): fixed inputs.
ADR-0090 D12 condition (c): fixed input.
The ruling: G
The CRM example hides the Pipeline entry from a member who cannot read opportunities with a visibility predicate on the navigation item. No capability is declared or granted, and the dashboard itself carries no requiredPermissions.
Fixed here:
examples/app-crm/src/apps/crm.app.ts, nav_pipeline_dashboard: add visible: "current_user.can('crm_opportunity', 'read')". Nothing else on the entry changes.
examples/app-crm/test/pipeline-audience.test.ts pins two things, read off the parsed stack: the Pipeline nav entry carries a visible predicate of the form current_user.can('<object>', 'read'), and that <object> equals the object of the dataset every pipeline_dashboard widget reads. The capability-centred cases are retired with the capability.
Verification before push: the example-crm package tests, and install-local-unmanaged-mode.dogfood.test.ts run locally once more, expected back to 20/20; both recorded in the PR body.
⛔ Not: a declared capability on the CRM stack (A — makes the package un-installable unmanaged); a grant with no declaration (A′ — condition (c)); a platform administrator bypass in holdsRequiredPermissions (D — changes ADR-0066's meaning); demoStaff / devLogins for this card (not needed under G; the seeded admin keeps the entry); any change under packages/qa/dogfood.
Why G, on the four axes (business standpoint)
① Long-term soundness (≥50%). The platform has two keys for two jobs, and this card's job is the hide. Using the refuse key cost the example its unmanaged installability and closed the dashboard to its own author in Studio; the hide key costs nothing the platform has not already paid for type: 'object' entries (objectui#12109 prunes those on the same payload). The data refusal stays the one enforcement.
② Measured business pull. The 18.3.0 RC walkthrough member ([sweep] v18.3.0 release test sweep #27377 W3-UX) sees the landing say nothing is available while the sidebar offers Pipeline; G closes that gap for every member without opportunity read.
③ AI-error prevention. G's failure direction: a frame with no permissions payload withholds the entry and reports once (objectui#12700), never shows it to everyone; a wrong object name in the predicate is caught by the new pin against the dataset's object. A and A′ failed loud too, but in a required CI gate that the PR's own CI had skipped.
④ Startup no-sprawl. Two files in examples/app-crm, S. A was M across packages/qa; C left the inconsistency in place.
Only-① pick: G. Do ②③④ flip it: no. Effort for the chosen letter: S.
Revision 2 (Ruling-ref: 6512784198), head a1cd6d8.
{
"issue": 27735,
"status": "done",
"branch": "claude/project-thread-ch022k",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27805",
"session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
"premise_still_valid": true,
"summary": "Revision 2 per ruling 6512784198 (letter G). nav_pipeline_dashboard gains visible current_user.can('crm_opportunity', 'read'); the first revision's capability, stack capabilities, crm_sales_user systemPermissions and both requiredPermissions are reverted, so the branch diff vs main is crm.app.ts (one entry) plus the new test. Earlier finding answered: seeded admin and unmanaged install unaffected, since the server read gate does not judge visible. PR retitled and body rewritten with Ruling-ref.",
"tests": "pnpm --filter @objectstack/example-crm test exit 0, 7/7 files, 48/48 tests (pipeline-audience.test.ts: 2 cases, read off the parsed stack, OBJECT derived from the widgets' dataset); typecheck exit 0. Dogfood install-local-unmanaged-mode.dogfood.test.ts exit 0, 20/20 at a1cd6d8d (was 11 failed / 9 passed at 87e10674). Ablation: predicate object crm_opportunity to crm_account via scripts/ablation-replace.mjs, anchor hit 1, second case red, restored blob == HEAD, git diff HEAD empty.",
"gates": "local at a1cd6d8d: dispatch-gates --ran 39 derived, 38 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3 prerequisite, CI's). Re-run after the build settled: check:dts-closure (first exit 1, mid-build tree and the newly merged driver-odata unlinked until pnpm install) exit 0; check:i18n-coverage (first exit 3) exit 0; check:type-check-debt (first exit 3, driver-odata build) exit 0. check-sdui-manifest exit 0. CI check-runs: in_progress at report time.",
"line_budget": "2 files vs main: crm.app.ts +6/-1, test +60",
"files_changed": [
"examples/app-crm/src/apps/crm.app.ts",
"examples/app-crm/test/pipeline-audience.test.ts"
],
"deviations": [
"PR title and body edited by issue_patch through the relay (ruling's execution step asks the card thread to retitle and amend); not a PATCH outside a ruling",
"PR attribution link names this card thread (accepted by the seat)",
"not run: the console nav guard that evaluates current_user.can (no renderer harness in the example), named in Acceptance notes"
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one dogfood file the seat named was run: 20/20",
"mcp_calls": "0",
"api_writes": "4 total on this card thread: pr_create (rev 1), os-dev-report 6511354813 (rev 1), issue_patch #27805 title+body run 40989829672 (read back identical 7347 bytes), this report. Plus git pushes.",
"open_questions": [],
"out_of_scope_findings": []
}
ACCEPT → PR #27805 comment 6513357745 (head a1cd6d8d), done to ruling G (6512784198). Contract review not owed (Clause-②: no, Metadata-protocol: none). Landing through the merge queue.
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 经营报表 | none | P②
Triage: p2 ·
domain:ui·area:reports· 2026-10-29T21:53Z⛔ Not a claim.
6508997538(Dashboard: a viewer whose analytics queries are all refused 403 sees a blank canvas under the title (CRM Pipeline Dashboard as a plain member), with no per-widget refusal #27491 item 5). The example app is graded with the subsystem it exercises, which is the dashboard and nav audience.pipeline_dashboardandnav_pipeline_dashboarddeclare the samerequiredPermissions, naming a capability that the CRM sales sets which readopportunity_metrics's object actually grant. Pin both: a plain member is not offered Pipeline, and a sales user still is.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Claim: PM loop round 3 · 2026-10-29T23:15Z
Session:
session_01Utp6CY2n1JmZZKrHgtp9jC(seat; implementation by a card thread,mode:thread)Account:
os-billBranch:
claude/project-thread-ch022k(the card thread's harness push branch, self-reported — the measured platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-27735Domain:
domain:uiSeat:
domain:ui#2File surface:
examples/app-crm/src/dashboards/pipeline.dashboard.ts,examples/app-crm/src/apps/crm.app.ts,examples/app-crm/src/security/**(one capability, granted incrm_sales_user),examples/app-crm/test/**, and — widened by the seat on the dev's stop-and-report —examples/app-crm/objectstack.config.ts(onecapabilities: [...]line plus its import, so the capability is DECLARED on the stack:delegated-admin-gate.tscondition (c) refuses an undeclaredsystemPermissionsname); no changeset (the example isprivate: true;skip-changeset); ⛔package.json(no new devDependency); ⛔packages/spec/**,packages/rest/**, everyui/**fileContainer & model:
S,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②: no (expected; the dev measures from the diff)
Metadata-protocol: none (expected; the dev measures from the diff)
Responsibility: the CRM example declaring no audience gate on its Pipeline dashboard and nav item | the server's
requiredPermissionsread gate (ADR-0066) and the nav guard are fixed inputs | every CRM member without a sales permission set who is offered Pipeline todayThread-read: 6509200885
Premise: holds as filed (the seat read
origin/mainwhile filing: neither declaration carriesrequiredPermissions, andcrm_sales_usergrants no capability)Serial constraints cleared: no open PR touches
examples/app-crm/**(seat read of open PRs' files); the renderer half is PR #27777 (#27491), not paired; seats 3 and 4 agreed to the splitDispatch order:
/mnt/project-files/ui-seat2/dispatch-27735.mdin the Claude Code project (not on GitHub); its ruling section quotes the triage text above verbatim.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
os-dev-report
{ "issue": 27735, "status": "done", "branch": "claude/project-thread-ch022k", "pr": "https://github.com/objectstack-ai/objectstack/pull/27805", "session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)", "premise_still_valid": true, "summary": "Added crm.view_pipeline (defineCapability, scope org, packageId com.example.crm) on the CRM stack's capabilities, granted it in crm_sales_user (systemPermissions), and declared requiredPermissions ['crm.view_pipeline'] on pipeline_dashboard and nav_pipeline_dashboard. Readers: createMetaItemReadGate dashboard arm (audience-permission 403) and filterAppForUserWithReason nav pruning, both in packages/rest/src/meta-item-read-gate.ts, against the union of the caller's sets' systemPermissions. The declaration (not only the grant) is required: plugin-security's organization boundary condition (c) refuses a set carrying an undeclared or platform-scoped capability. objectstack.config.ts added to the surface by the seat (claim amended). Named gap: the example has no @objectstack/rest dependency, so tests pin every input the gate reads, not the gate run (seat declined the devDependency).", "tests": "pnpm --filter @objectstack/example-crm test: exit 0, Test Files 7/7, Tests 50/50 (new test/pipeline-audience.test.ts, 4 cases); typecheck exit 0; objectstack validate exit 0. Ablation (no build/dist path: example reads its own src): scripts/ablation-replace.mjs deleting the crm_sales_user systemPermissions line, anchor hit 1, holders case red (1 failed | 3 passed), restored blob == HEAD 314586f62842, git diff HEAD empty.", "gates": "local at 87e10674: dispatch-gates --ran: 39 derived, 38 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, needs every package dist; CI's). check:i18n-coverage first exit 3 (missing connector-slack dist), rerun after its named prerequisite build: exit 0 OK none new. node scripts/check-sdui-manifest.mjs exit 0. CI check-runs: in_progress at report time.", "line_budget": "7 files, +~150 (one new test file ~80, new capabilities.ts ~35, declarations ~15)", "files_changed": [ "examples/app-crm/objectstack.config.ts", "examples/app-crm/src/apps/crm.app.ts", "examples/app-crm/src/dashboards/pipeline.dashboard.ts", "examples/app-crm/src/security/capabilities.ts", "examples/app-crm/src/security/index.ts", "examples/app-crm/src/security/sales-positions.ts", "examples/app-crm/test/pipeline-audience.test.ts" ], "deviations": [ "objectstack.config.ts outside the dispatched surface: stopped and asked; seat granted and amended claim 6510769461 before PR", "PR attribution link names this card thread (session-context requires it verbatim), not the seat thread link the dispatch spelled", "first branch push (WIP) went out before the seat's surface answer; it carried the config line", "no changeset: @objectstack/example-crm is private; skip-changeset label on pr_create (read back present)", "harness post-PR assign/review step skipped per dispatch; PR has no assignee and no reviewer" ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "2 — relay pr_create (draft, labels skip-changeset) run 40976175747, body read back identical 7309 bytes; this os-dev-report comment. Plus git pushes (not REST).", "open_questions": [], "out_of_scope_findings": [ "none filed. Noted: packages/spec/src/ui/dashboard.zod.ts JSDoc near requiredPermissions still says not enforced (already recorded by the #27491 review thread); carrier: #27491 ruling thread · noted, not filed" ] }objectstack-fleet commentedon Oct 30, 2026
director#1 · os-bill · session_01Utp6CavdFhhfMVhuGjp7tP
Ruling: the CRM Pipeline entry's audience gate, after the measured cost of a declared capability (project discussion thread 「决定仪表盘全部无权限时的显示」, follow-up to #27491 ruling
6508997538item 5) · letter G · maintainer 「同意」 2026-10-30T01:36ZThe maintainer's own ruling, given in the project thread at 2026-10-30T01:34Z on the review thread's six-section brief and its post-measurement addendum (
/mnt/project-files/decisions/27735-种子管理员失去Pipeline-呈批.md, recommendation G). Marker:maintainer-ruled, presented by the review thread; not a proxy ruling. It amends the MECHANISM of #27491 ruling6508997538item 5; that item's intent (a member who cannot read the Pipeline's data is not offered it) stands.What was measured and re-verified
87e10674(declared capabilitycrm.view_pipeline, granted tocrm_sales_user, required onpipeline_dashboardandnav_pipeline_dashboard). The card thread ranpackages/qa/dogfood/test/install-local-unmanaged-mode.dogfood.test.tsagainst it: 11 failed / 9 passed; the control (the CRM at the merge-base, same build) 20/20. PR CI was green only because the dogfood shards were skipped on the PR.capabilitiesdeclaration makes the CRM package refuse an UNMANAGED install, 422UNMANAGED_INSTALL_UNCOPYABLE. By design: thecapabilitymetadata type isallowRuntimeCreate: false(packages/spec/src/kernel/metadata-plugin.zod.tsL1242, ADR-0131 D3 / ADR-0066 D1: a capability lives in the registry and is never an environment row), and the unmanaged import refuses whole any body carrying a runtime-uncreatable type (packages/metadata-protocol/src/package-unmanaged-install.tsL33). The dogfood uses the CRM as its unmanaged-install specimen. A package that declares capabilities cannot be installed unmanaged; this card does not change that.GET /meta/dashboard/pipeline_dashboardanswers 403audience-permission.holdsRequiredPermissions(packages/rest/src/meta-item-read-gate.tsL753) has no administrator bypass; the dashboard arm (L1690–1700) judges the audience gate beforepolicy.arms, author or not, so the gate also closes the dashboard in Studio for a caller who lacks the capability. The seeded admin holdsADMIN_FULL_ACCESS_CAPABILITIES(eval-user.zod.tsL115): every platform capability, no app-declared one (system/dev-login.zod.tsL14–27 documents this as by construction).packages/plugins/plugin-security/src/delegated-admin-gate.tsL875–906, organization boundary condition (c): asystemPermissionsname that is not a capability declaredscope: 'org'puts the set outside the boundary, so an organization administrator could no longer assigncrm_sales_useror the three sales positions that distribute it (ADR-0090 D12).current_user.can(object, verb)from the member's permissions payload (objectui#4421;ui/packages/app-shell/src/hooks/useNavMemberGates.ts,ExpressionProvider.tsx), andBaseNavItemSchema.visibleis a CEL predicate.opportunity_metricsis bound tocrm_opportunity(examples/app-crm/src/datasets/opportunity.dataset.tsL17), the object whose read refusal is the six tiles' 403.visibleis not judged by the server's read gate (meta-item-read-gate.tsL918), so the dogfood's admin reads are untouched.Governing text
visibleWhenhides,requiredPermissionsrefuses." This card's need is HIDE (the entry is not offered); the enforcement is the dataset query's 403, already in place and unchanged.6508997538item 5 (intent: a member without a sales set is not offered the Pipeline; a sales user is) — mechanism amended here.package-unmanaged-install.ts): fixed inputs.The ruling: G
The CRM example hides the Pipeline entry from a member who cannot read opportunities with a visibility predicate on the navigation item. No capability is declared or granted, and the dashboard itself carries no
requiredPermissions.Fixed here:
examples/app-crm/src/apps/crm.app.ts,nav_pipeline_dashboard: addvisible: "current_user.can('crm_opportunity', 'read')". Nothing else on the entry changes.src/security/capabilities.ts(delete),src/security/index.ts,objectstack.config.ts(capabilities),src/security/sales-positions.ts(systemPermissions),src/dashboards/pipeline.dashboard.ts(requiredPermissions).examples/app-crm/test/pipeline-audience.test.tspins two things, read off the parsed stack: the Pipeline nav entry carries avisiblepredicate of the formcurrent_user.can('<object>', 'read'), and that<object>equals theobjectof the dataset everypipeline_dashboardwidget reads. The capability-centred cases are retired with the capability.requiredPermissionsonpipeline_dashboardandnav_pipeline_dashboard), so a member who cannot read its data is not offered it #27735 reads: a member who cannot readcrm_opportunitysees no Pipeline entry in the CRM sidebar; a sales user and the seeded administrator see it as today; a direct link for the refused member shows the Dashboard: a viewer whose analytics queries are all refused 403 sees a blank canvas under the title (CRM Pipeline Dashboard as a plain member), with no per-widget refusal #27491 no-access panel (PR fix(plugin-dashboard): a viewer whose every tile is refused sees one no-access panel, not a blank canvas #27777), not a blank. The earlier "direct link getsdashboard-access-denied" acceptance is withdrawn.example-crmpackage tests, andinstall-local-unmanaged-mode.dogfood.test.tsrun locally once more, expected back to 20/20; both recorded in the PR body.holdsRequiredPermissions(D — changes ADR-0066's meaning);demoStaff/devLoginsfor this card (not needed under G; the seeded admin keeps the entry); any change underpackages/qa/dogfood.Why G, on the four axes (business standpoint)
type: 'object'entries (objectui#12109 prunes those on the same payload). The data refusal stays the one enforcement.object. A and A′ failed loud too, but in a required CI gate that the PR's own CI had skipped.examples/app-crm, S. A was M acrosspackages/qa; C left the inconsistency in place.Only-① pick: G. Do ②③④ flip it: no. Effort for the chosen letter: S.
① 长远:藏用
visible、拒用requiredPermissions(ADR-0066 D4 原话),本卡要的是藏;声明能力让 CRM 不能非托管安装、作者在 Studio 也打不开,代价不是这张卡该付的;对象类导航条目早已按同一权限载荷裁剪(objectui#12109)。② 拉动:RC 走查成员(#27377 W3-UX)首页说没东西、侧栏却挂 Pipeline;G 对每个读不到机会的成员都关上这个口。
③ 防错:无权限载荷的框架按 objectui#12700 收起条目不报错;谓词里的对象名由新钉对照数据集
object;A/A′ 的红出现在 PR CI 跳过的必需门里,是 dogfood 实跑才看见的。④ 不扩散:两个文件,S;A 要跨
packages/qa换样本(M);C 留着不一致。Prior rulings read: #27491
6508997538item 5(本裁修订其机制)、ADR-0066 D1/D4、ADR-0131 D3、ADR-0090 D12 条件 (c)、objectui#4421 / #12109 / #12700、hotclm#28 D(demoStaff,本裁不用);实测:卡线程 dogfood 11/9 与对照 20/20。Execution after the ruling
pm:dispatchedondomain:ui#2; the card thread amends PR fix(app-crm): hide the Pipeline nav entry from a member who cannot read opportunities #27805 per items 1–5 withRuling-ref: <this comment id>, retitles the PR, keepsFixes #27735, then gates, os-dev-report and the seat's ACCEPT + PASS flow. Non-governed surface.6508997538: item 5's mechanism is now this ruling'svisiblepredicate; the all-refused panel (PR fix(plugin-dashboard): a viewer whose every tile is refused sees one no-access panel, not a blank canvas #27777) is unchanged.allowRuntimeCreate: false; the unmanaged-install refusal;holdsRequiredPermissions; the organization boundary; the Dashboard: a viewer whose analytics queries are all refused 403 sees a blank canvas under the title (CRM Pipeline Dashboard as a plain member), with no per-widget refusal #27491 panel.Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
os-dev-report
Revision 2 (Ruling-ref: 6512784198), head a1cd6d8.
{ "issue": 27735, "status": "done", "branch": "claude/project-thread-ch022k", "pr": "https://github.com/objectstack-ai/objectstack/pull/27805", "session": "session_01Utp6CXo8iLQ8vn2fXj6wxp (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)", "premise_still_valid": true, "summary": "Revision 2 per ruling 6512784198 (letter G). nav_pipeline_dashboard gains visible current_user.can('crm_opportunity', 'read'); the first revision's capability, stack capabilities, crm_sales_user systemPermissions and both requiredPermissions are reverted, so the branch diff vs main is crm.app.ts (one entry) plus the new test. Earlier finding answered: seeded admin and unmanaged install unaffected, since the server read gate does not judge visible. PR retitled and body rewritten with Ruling-ref.", "tests": "pnpm --filter @objectstack/example-crm test exit 0, 7/7 files, 48/48 tests (pipeline-audience.test.ts: 2 cases, read off the parsed stack, OBJECT derived from the widgets' dataset); typecheck exit 0. Dogfood install-local-unmanaged-mode.dogfood.test.ts exit 0, 20/20 at a1cd6d8d (was 11 failed / 9 passed at 87e10674). Ablation: predicate object crm_opportunity to crm_account via scripts/ablation-replace.mjs, anchor hit 1, second case red, restored blob == HEAD, git diff HEAD empty.", "gates": "local at a1cd6d8d: dispatch-gates --ran 39 derived, 38 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3 prerequisite, CI's). Re-run after the build settled: check:dts-closure (first exit 1, mid-build tree and the newly merged driver-odata unlinked until pnpm install) exit 0; check:i18n-coverage (first exit 3) exit 0; check:type-check-debt (first exit 3, driver-odata build) exit 0. check-sdui-manifest exit 0. CI check-runs: in_progress at report time.", "line_budget": "2 files vs main: crm.app.ts +6/-1, test +60", "files_changed": [ "examples/app-crm/src/apps/crm.app.ts", "examples/app-crm/test/pipeline-audience.test.ts" ], "deviations": [ "PR title and body edited by issue_patch through the relay (ruling's execution step asks the card thread to retitle and amend); not a PATCH outside a ruling", "PR attribution link names this card thread (accepted by the seat)", "not run: the console nav guard that evaluates current_user.can (no renderer harness in the example), named in Acceptance notes" ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one dogfood file the seat named was run: 20/20", "mcp_calls": "0", "api_writes": "4 total on this card thread: pr_create (rev 1), os-dev-report 6511354813 (rev 1), issue_patch #27805 title+body run 40989829672 (read back identical 7347 bytes), this report. Plus git pushes.", "open_questions": [], "out_of_scope_findings": [] }objectstack-fleet commentedon Oct 30, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
ACCEPT → PR #27805 comment 6513357745 (head
a1cd6d8d), done to ruling G (6512784198). Contract review not owed (Clause-②: no,Metadata-protocol: none). Landing through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Landed · 2026-10-30T02:38Z
da94c11337fdb4c37ee809e9748a76791a5199cccompletedby the PR'sFixeslineState:
pm:dispatchedremoved; the assignee stays as the record of who carried it.Generated by Claude Code