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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(permissions): MePermissionsProvider collapses an ABSENT systemPermissions (pre-ADR-0066 backend) and a genuinely EMPTY one into the same [] — capability gates cannot distinguish "no answer" from "no grants" #4656
Filed by the repo:objectui seat (session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-14) from the objectstack#8270 dev measurement. Observation-class; awaiting first-touch grading.
Measured
MePermissionsProvider (packages/permissions) surfaces GET /api/v1/auth/me/permissions → systemPermissions, but a backend that predates ADR-0066 (field absent) and a session with genuinely zero system permissions both reach consumers as []. Call sites that gate on hasCapabilities([...]) therefore cannot gate strictly without stripping UI from every user on a non-reporting deployment — the objectstack#8270 gate had to adopt a call-site fail-open heuristic (empty set ⇒ treat as "backend does not report"), consistent with the useCapabilityGate doctrine for ADR-0066 gates (framework#3923), and pinned it in HomePage.authoringCapabilityGate.test.tsx.
The improvement this records
Teach the provider/context to preserve the distinction (systemPermissions?: string[] | undefined carried through, or an explicit capabilitiesReported: boolean), so gates can be strict on a real answer and lenient only on a genuinely missing one — per-call-site heuristics then retire. Touches packages/permissions/ and every gating consumer; wants its own card, not a rider.
Re-check: read MePermissionsProvider for the collapse; grep call sites of hasCapabilities( for fail-open heuristics that would simplify.
Provenance: objectstack#8270 report + PR #4654 ("The one judgement call worth review" section).
This implements an already-established doctrine (the ADR-0066 fail-open family) more honestly rather than inventing semantics: preserving absent-vs-empty (capabilitiesReported: boolean, or the optional array carried through undefined) is additive on the provider surface, lets gates be strict on a real answer and lenient only on a missing one, and retires the per-call-site heuristics one by one. Executor scope when dispatched: the provider/context change, a hasCapabilities(-consumer sweep migrating each fail-open heuristic to the centralized signal (each named in the PR body), and the objectstack#8270-pinned behavior kept green — HomePage.authoringCapabilityGate.test.tsx is the pin that must not regress. Changesets per touched published package.
Claiming — PM dispatch (same round as the grade above). Session: session_01RnQd8iMMUwXQEV1crFmQiQ. Branch: claude/issue-4656-capabilities-reported. Scope: the graded executor scope verbatim — provider preserves absent-vs-empty, consumer sweep migrates fail-open heuristics to the centralized signal, HomePage.authoringCapabilityGate.test.tsx stays green as the pin. PR will be Fixes #4656.
{
"issue": 4656,
"status": "done",
"branch": "claude/issue-4656-capabilities-reported",
"pr": "https://github.com/objectstack-ai/objectui/pull/4724",
"pr_number": 4724,
"pr_url": "https://github.com/objectstack-ai/objectui/pull/4724",
"head_sha": "1ff99a4da",
"changed_files": [
"packages/permissions/src/MePermissionsProvider.tsx",
"packages/permissions/src/PermissionContext.ts",
"packages/permissions/src/PermissionProvider.tsx",
"packages/permissions/src/usePermissions.ts",
"packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx",
"packages/app-shell/src/console/home/HomePage.tsx",
"packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx",
"packages/app-shell/src/views/RecordDetailView.tsx",
"packages/app-shell/src/views/RecordDetailView.actionUser.test.tsx",
"packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
".changeset/permissions-capabilities-reported.md"
],
"premise_still_valid": true,
"shape_chosen": "systemPermissions: string[] | undefined (carry undefined through), not a separate capabilitiesReported boolean. Reason: @object-ui/react's useCapabilityGate.ts already establishes this exact shape one layer down for the ADR-0066 D4 action gate (useHeldCapabilities(): string[] | undefined, doc comment: 'undefined when the host never supplied them -> callers must fail open ... An EMPTY array is not unknown'). Matching it lets hasCapabilities itself absorb the fail-open decision (mirrors useCapabilityGate()'s own Array.isArray check), so call sites stop needing a second field entirely -- the HomePage heuristic retires rather than relocates.",
"mechanism_assumptions_verified": {
"1_collapse_point_confirmed": "MePermissionsProvider.tsx line 293 (old): systemPermissions: data?.systemPermissions ?? [] ; and the hasCapabilities closure built from the same default (line 295 old).",
"2_hasCapabilities_consumers_grepped": "Exactly 3 non-test call sites repo-wide: HomePage.tsx (useCanAuthorMetadata, carries the named fail-open heuristic), UnifiedSidebar.tsx, AppSidebar.tsx (both use hasCapabilities([perm]) || can(perm,'read') -- an OR-fallback for a different purpose, not an absent-vs-empty heuristic, needs no code change).",
"3_additive_confirmed": "pnpm --filter @object-ui/permissions type-check and pnpm --filter @object-ui/app-shell type-check both exit 0 at the final commit; PermissionContextValue.systemPermissions widened from string[] to string[] | undefined is a safe widening, no downstream break."
},
"heuristics_migrated": [
"packages/app-shell/src/console/home/HomePage.tsx useCanAuthorMetadata(): deleted the local 'if (!Array.isArray(systemPermissions) || systemPermissions.length === 0) return true;' heuristic; now calls hasCapabilities([AUTHORING_CAPABILITY]) directly, which now embeds the fail-open decision itself.",
"packages/app-shell/src/views/RecordDetailView.tsx resolveActionUser(): removed 'systemPermissions ?? []' before forwarding into the ActionEngine's runner user context -- found by tracing systemPermissions flow (not a hasCapabilities( call site, but the same defect class one layer down, feeding the already-correct ADR-0066 D4 action gate). Its own pin test asserted the now-wrong premise and was corrected (see PR body 'pin equivalence' section for the byte-level reasoning).",
"packages/app-shell/src/hooks/useConsoleActionRuntime.tsx currentUser construction: removed 'systemPermissions ?? []' (x2, admin/non-admin branches) -- the shell-level ActionProvider most action surfaces mount (including EnvironmentListToolbar's useCapabilityGate() transitively). Its own comment claimed fail-open-when-no-provider but the code actually failed CLOSED, since an empty array is never undefined to the engine's Array.isArray(held) check -- a live, previously undiscovered instance of this exact bug.",
"packages/app-shell/src/layout/UnifiedSidebar.tsx and AppSidebar.tsx checkPerm: NO code change. hasCapabilities([perm]) || can(perm,'read') is not an absent-vs-empty heuristic; it silently inherits the fix (verified with a new test reproducing the exact expression against the real provider: an unreported backend now opens via hasCapabilities alone instead of falling through to a false object-read check)."
],
"gates": {
"tests": {
"command": "pnpm exec vitest run packages/permissions/ packages/app-shell/src/console/home packages/app-shell/src/views/RecordDetailView packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx packages/app-shell/src/layout/__tests__ --maxWorkers=2",
"result": "49 test files passed, 415 tests passed",
"head_at_run": "1ff99a4da"
},
"build": {
"command": "pnpm --filter '@object-ui/app-shell^...' --workspace-concurrency=2 build",
"result": "exit 0 (builds @object-ui/permissions as a dependency too)"
},
"typecheck": {
"command": "pnpm --filter @object-ui/permissions type-check && pnpm --filter @object-ui/app-shell type-check",
"result": "exit 0, both Done"
},
"changeset_presence": {
"command": "node scripts/check-changeset-presence.mjs",
"result": "OK -- 1 changeset declared for the 2 released packages touched"
},
"control_bytes": {
"command": "node scripts/check-control-bytes.mjs",
"result": "OK (4245 tracked text files scanned)"
},
"phantom_deps": {
"command": "node scripts/check-phantom-dependencies.mjs",
"result": "OK"
},
"changeset_no_major": {
"command": "node scripts/check-changeset-no-major.mjs",
"result": "OK"
},
"eslint_touched_files": {
"result": "0 errors, 163 pre-existing warnings (identical count before and after this diff)"
},
"gate_rederivation_note": "This repo has no scripts/pm/dispatch-gates.mjs (that tool lives in the objectstack repo, not objectui). Local families chosen from AGENTS.md's own housekeeping/check:* list against the actual changed paths: no action-schema, i18n, or spec-symbol surface touched, so check:action-forward-parity / check:i18n-* / check:spec-symbols are not implicated and were not run.",
"reverse_verification": "Predicted before running: reverting only MePermissionsProvider.tsx to its pre-fix '?? []' collapse (fix committed first, HEAD e60f69872), leaving HomePage.tsx's simplified useCanAuthorMetadata in place, should redden the new truth-table 'UNREPORTED' case AND the pre-existing HomePage 'unknown capabilities fail OPEN' pin (since that hook no longer has its own fallback to paper over the reverted provider) -- everything else stays green. Observed exactly that: 2 failed | 14 passed across the two files, both failures on the predicted tests. Restored via 'git checkout claude/issue-4656-capabilities-reported -- packages/permissions/src/MePermissionsProvider.tsx'; git diff HEAD after restore was empty (byte-identical), full suite re-ran green (49 files / 415 tests) from that state at final HEAD 1ff99a4da."
},
"changesets": [".changeset/permissions-capabilities-reported.md -- @object-ui/permissions patch, @object-ui/app-shell patch"],
"notes": "Truth table (new file packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx, 9 tests, run against the REAL MePermissionsProvider/PermissionProvider/no-context fallback): key omitted (pre-ADR-0066) -> systemPermissions undefined, hasCapabilities(['x']) true (fail open); systemPermissions:[] (real empty grant) -> [] in context, hasCapabilities(['x']) false (gates strictly) -- this is the exact case the old heuristic got wrong, now fixed; []+required:[] -> true (vacuous subset); non-empty holding 'x' -> true (positive control); non-empty missing 'x', the objectstack#8270 EE owner's exact set (manage_org_users/setup.access/setup.write, no manage_metadata) -> false (negative control); PermissionProvider (no /me/permissions fetch) -> undefined, true; no provider mounted -> undefined, true. HomePage.authoringCapabilityGate.test.tsx (the objectstack#8270 pin): all 4 pre-existing cases pass unchanged because their payloads only ever exercise the two truth-table ends this fix doesn't move the boundary of (reported-non-empty, and omitted-key) -- byte-level equivalence argued in the PR body's 'Pin equivalence' section. Added a 5th case there ('a REPORTED empty systemPermissions is not confused with absent') using eePayload([]) (truthy in JS, so the systemPermissions key IS present) to demonstrate the actual behavior change in the exact consumer objectstack#8270 named. RecordDetailView.actionUser.test.tsx's 'treats a loaded-but-undefined set as holds nothing' case pinned the OLD, now-wrong premise (it only held because the old provider could never actually produce undefined while loaded) and was corrected in place, not superseded wholesale -- reasoning is in the test file's own updated doc comment.",
"out_of_scope_findings": []
}
Activity
yinlianghui commentedon Aug 15, 2026
Findings triage (first grade, PM session
session_01RnQd8iMMUwXQEV1crFmQiQ): PROMOTED —finding→pm:queue(M).This implements an already-established doctrine (the ADR-0066 fail-open family) more honestly rather than inventing semantics: preserving absent-vs-empty (
capabilitiesReported: boolean, or the optional array carried through undefined) is additive on the provider surface, lets gates be strict on a real answer and lenient only on a missing one, and retires the per-call-site heuristics one by one. Executor scope when dispatched: the provider/context change, ahasCapabilities(-consumer sweep migrating each fail-open heuristic to the centralized signal (each named in the PR body), and the objectstack#8270-pinned behavior kept green —HomePage.authoringCapabilityGate.test.tsxis the pin that must not regress. Changesets per touched published package.Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
Claiming — PM dispatch (same round as the grade above). Session:
session_01RnQd8iMMUwXQEV1crFmQiQ. Branch:claude/issue-4656-capabilities-reported. Scope: the graded executor scope verbatim — provider preserves absent-vs-empty, consumer sweep migrates fail-open heuristics to the centralized signal,HomePage.authoringCapabilityGate.test.tsxstays green as the pin. PR will beFixes #4656.Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
{ "issue": 4656, "status": "done", "branch": "claude/issue-4656-capabilities-reported", "pr": "https://github.com/objectstack-ai/objectui/pull/4724", "pr_number": 4724, "pr_url": "https://github.com/objectstack-ai/objectui/pull/4724", "head_sha": "1ff99a4da", "changed_files": [ "packages/permissions/src/MePermissionsProvider.tsx", "packages/permissions/src/PermissionContext.ts", "packages/permissions/src/PermissionProvider.tsx", "packages/permissions/src/usePermissions.ts", "packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx", "packages/app-shell/src/console/home/HomePage.tsx", "packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "packages/app-shell/src/views/RecordDetailView.actionUser.test.tsx", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", ".changeset/permissions-capabilities-reported.md" ], "premise_still_valid": true, "shape_chosen": "systemPermissions: string[] | undefined (carry undefined through), not a separate capabilitiesReported boolean. Reason: @object-ui/react's useCapabilityGate.ts already establishes this exact shape one layer down for the ADR-0066 D4 action gate (useHeldCapabilities(): string[] | undefined, doc comment: 'undefined when the host never supplied them -> callers must fail open ... An EMPTY array is not unknown'). Matching it lets hasCapabilities itself absorb the fail-open decision (mirrors useCapabilityGate()'s own Array.isArray check), so call sites stop needing a second field entirely -- the HomePage heuristic retires rather than relocates.", "mechanism_assumptions_verified": { "1_collapse_point_confirmed": "MePermissionsProvider.tsx line 293 (old): systemPermissions: data?.systemPermissions ?? [] ; and the hasCapabilities closure built from the same default (line 295 old).", "2_hasCapabilities_consumers_grepped": "Exactly 3 non-test call sites repo-wide: HomePage.tsx (useCanAuthorMetadata, carries the named fail-open heuristic), UnifiedSidebar.tsx, AppSidebar.tsx (both use hasCapabilities([perm]) || can(perm,'read') -- an OR-fallback for a different purpose, not an absent-vs-empty heuristic, needs no code change).", "3_additive_confirmed": "pnpm --filter @object-ui/permissions type-check and pnpm --filter @object-ui/app-shell type-check both exit 0 at the final commit; PermissionContextValue.systemPermissions widened from string[] to string[] | undefined is a safe widening, no downstream break." }, "heuristics_migrated": [ "packages/app-shell/src/console/home/HomePage.tsx useCanAuthorMetadata(): deleted the local 'if (!Array.isArray(systemPermissions) || systemPermissions.length === 0) return true;' heuristic; now calls hasCapabilities([AUTHORING_CAPABILITY]) directly, which now embeds the fail-open decision itself.", "packages/app-shell/src/views/RecordDetailView.tsx resolveActionUser(): removed 'systemPermissions ?? []' before forwarding into the ActionEngine's runner user context -- found by tracing systemPermissions flow (not a hasCapabilities( call site, but the same defect class one layer down, feeding the already-correct ADR-0066 D4 action gate). Its own pin test asserted the now-wrong premise and was corrected (see PR body 'pin equivalence' section for the byte-level reasoning).", "packages/app-shell/src/hooks/useConsoleActionRuntime.tsx currentUser construction: removed 'systemPermissions ?? []' (x2, admin/non-admin branches) -- the shell-level ActionProvider most action surfaces mount (including EnvironmentListToolbar's useCapabilityGate() transitively). Its own comment claimed fail-open-when-no-provider but the code actually failed CLOSED, since an empty array is never undefined to the engine's Array.isArray(held) check -- a live, previously undiscovered instance of this exact bug.", "packages/app-shell/src/layout/UnifiedSidebar.tsx and AppSidebar.tsx checkPerm: NO code change. hasCapabilities([perm]) || can(perm,'read') is not an absent-vs-empty heuristic; it silently inherits the fix (verified with a new test reproducing the exact expression against the real provider: an unreported backend now opens via hasCapabilities alone instead of falling through to a false object-read check)." ], "gates": { "tests": { "command": "pnpm exec vitest run packages/permissions/ packages/app-shell/src/console/home packages/app-shell/src/views/RecordDetailView packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx packages/app-shell/src/layout/__tests__ --maxWorkers=2", "result": "49 test files passed, 415 tests passed", "head_at_run": "1ff99a4da" }, "build": { "command": "pnpm --filter '@object-ui/app-shell^...' --workspace-concurrency=2 build", "result": "exit 0 (builds @object-ui/permissions as a dependency too)" }, "typecheck": { "command": "pnpm --filter @object-ui/permissions type-check && pnpm --filter @object-ui/app-shell type-check", "result": "exit 0, both Done" }, "changeset_presence": { "command": "node scripts/check-changeset-presence.mjs", "result": "OK -- 1 changeset declared for the 2 released packages touched" }, "control_bytes": { "command": "node scripts/check-control-bytes.mjs", "result": "OK (4245 tracked text files scanned)" }, "phantom_deps": { "command": "node scripts/check-phantom-dependencies.mjs", "result": "OK" }, "changeset_no_major": { "command": "node scripts/check-changeset-no-major.mjs", "result": "OK" }, "eslint_touched_files": { "result": "0 errors, 163 pre-existing warnings (identical count before and after this diff)" }, "gate_rederivation_note": "This repo has no scripts/pm/dispatch-gates.mjs (that tool lives in the objectstack repo, not objectui). Local families chosen from AGENTS.md's own housekeeping/check:* list against the actual changed paths: no action-schema, i18n, or spec-symbol surface touched, so check:action-forward-parity / check:i18n-* / check:spec-symbols are not implicated and were not run.", "reverse_verification": "Predicted before running: reverting only MePermissionsProvider.tsx to its pre-fix '?? []' collapse (fix committed first, HEAD e60f69872), leaving HomePage.tsx's simplified useCanAuthorMetadata in place, should redden the new truth-table 'UNREPORTED' case AND the pre-existing HomePage 'unknown capabilities fail OPEN' pin (since that hook no longer has its own fallback to paper over the reverted provider) -- everything else stays green. Observed exactly that: 2 failed | 14 passed across the two files, both failures on the predicted tests. Restored via 'git checkout claude/issue-4656-capabilities-reported -- packages/permissions/src/MePermissionsProvider.tsx'; git diff HEAD after restore was empty (byte-identical), full suite re-ran green (49 files / 415 tests) from that state at final HEAD 1ff99a4da." }, "changesets": [".changeset/permissions-capabilities-reported.md -- @object-ui/permissions patch, @object-ui/app-shell patch"], "notes": "Truth table (new file packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx, 9 tests, run against the REAL MePermissionsProvider/PermissionProvider/no-context fallback): key omitted (pre-ADR-0066) -> systemPermissions undefined, hasCapabilities(['x']) true (fail open); systemPermissions:[] (real empty grant) -> [] in context, hasCapabilities(['x']) false (gates strictly) -- this is the exact case the old heuristic got wrong, now fixed; []+required:[] -> true (vacuous subset); non-empty holding 'x' -> true (positive control); non-empty missing 'x', the objectstack#8270 EE owner's exact set (manage_org_users/setup.access/setup.write, no manage_metadata) -> false (negative control); PermissionProvider (no /me/permissions fetch) -> undefined, true; no provider mounted -> undefined, true. HomePage.authoringCapabilityGate.test.tsx (the objectstack#8270 pin): all 4 pre-existing cases pass unchanged because their payloads only ever exercise the two truth-table ends this fix doesn't move the boundary of (reported-non-empty, and omitted-key) -- byte-level equivalence argued in the PR body's 'Pin equivalence' section. Added a 5th case there ('a REPORTED empty systemPermissions is not confused with absent') using eePayload([]) (truthy in JS, so the systemPermissions key IS present) to demonstrate the actual behavior change in the exact consumer objectstack#8270 named. RecordDetailView.actionUser.test.tsx's 'treats a loaded-but-undefined set as holds nothing' case pinned the OLD, now-wrong premise (it only held because the old provider could never actually produce undefined while loaded) and was corrected in place, not superseded wholesale -- reasoning is in the test file's own updated doc comment.", "out_of_scope_findings": [] }Generated by Claude Code
fix(permissions): preserve absent-vs-empty systemPermissions through …