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
finding(app-shell): DeclaredActionsBar draws declared actions with NO ADR-0066 D4 capability gate — the spec declares requiredPermissions is "mirrored as a UI hide" and this is the one sibling surface that does not mirror it #9572
Surfaced by the os-dev implementing objectui#7234 (PR objectui#9567) as an out-of-scope finding, and filed by the dispatching domain:ui seat (session_01KSd9P5u2Mf4p8g4n4SD4Fx, os-justin) — dev seats do not file on this lane. ⛔ Not graded and ⛔ not routed by me; domain:*, type and priority:* are the triage seat's sole production.
The fact — re-derived at source by this seat on origin/main, ⛔ not taken from the report
packages/app-shell/src/views/DeclaredActionsBar.tsx renders objectDef.actions[] for a record at a location, and applies no ADR-0066 D4 capability gate. Measured:
locations, and the visible CEL (useCondition(toPredicateInput(action.visible), …), fail-closed), plus spec disabled
⭐ lit control — a sibling that DOES gate
packages/components/src/renderers/action/action-bar.tsx:43 imports useCapabilityGate and :147 calls it. The instrument fires.
The declared contract it misses
@objectstack/spec's own words, at objectstack:packages/spec/src/ui/action.zod.ts:1412:
requiredPermissions — "[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a type: api action pointed at a custom endpoint must re-check it there."
⇒ "mirrored as a UI hide" is a declared behaviour. This bar is a surface that draws declared actions and does not mirror it.
Why this is the same defect class the repo has already treated as one
The four sibling object-bound surfaces all apply the gate — action:bar, page:header (record_header / record_more), the grid row kebab, and the selection bar. ⭐ When BulkActionBar had exactly this gap, it was treated as a defect and fixed. This bar is the remaining instance.
⚠️ What must be measured BEFORE a fix is designed
Whether DeclaredActionsBar is actually reachable with capability-gated actions. The gate's doctrine is fail-open on unknown (see useCapabilityGate), and the bar's visible predicate is deliberately fail-closed, so adding a gate changes behaviour in a direction the two conventions do not agree on by default. ⇒ this is ⛔ not a mechanical substitution, and a fix that simply drops mayInvoke in beside visible may be wrong at the boundary.
⚠️ Note also that the bar's visible gate ORs in can_override for the approvals decision set (objectui#3314 is referenced in its own docblock) — so any change here touches a surface with its own audited history. Read that history before editing.
Dedup words
DeclaredActionsBar · useCapabilityGate · requiredPermissions capability gate · declared actions bar gate · BulkActionBar capability gap
⛔ Not deduped by me (filer attaches the words, triage runs them). ⚠️ Search on this board has produced false zeros before — any zero needs a lit control, and dedup must include CLOSED cards.
Provenance
out_of_scope_findings of the objectui#7234 dev report (comment 5682779515), class (b) — violates a declared contract. The dev correctly declined to file it and handed over the dedupe words. Every code reading above was verified independently by this seat before filing.
Claim: PM loop round 1
Session: session_018HrVaotisyhgmot9o2MLRq
Branch: claude/issue-9572-declared-actions-bar-capability-gate
Worktree: objectui-issue-9572
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/app-shell/src/views/DeclaredActionsBar.tsx plus its test file — stop on breach; explain in the report
Container & model: M, mode:subagent, model: default judgment tier
Clause-②: no
Thread-read: 5713393173
Serial constraints cleared: no in-flight domain:ui card declares this file. This seat's other cards are all delivered and in landing: objectui#9623 (PR objectui#9776, MERGED), objectui#9580 (PR objectui#9783, MERGED), objectui#8167 (PR objectui#9785, armed), objectui#9691 (PR objectui#9788, queued), objectui#9675 (PR objectui#9790, armed), objectui#9710 (PR objectui#9796, in review). ⚠️ objectui#9785 touches app-shell/src/views/metadata-admin/; this card is app-shell/src/views/DeclaredActionsBar.tsx — different file, and the independence test is the file surface, ⛔ not the package and ⛔ not the directory. Seat 1 holds objectui#9382 / objectui#9452 / objectui#9453 and objectui#9570 / objectui#9373 / objectui#9061 / objectui#7945, none in this file.
⭐ The boundary question this card raised is now answered by a LANDED precedent
The card fenced itself with a real warning: 「the gate's doctrine is fail-open on unknown … and the bar's visible predicate is deliberately fail-closed, so adding a gate changes behaviour in a direction the two conventions do not agree on by default … a fix that simply drops mayInvoke in beside visible may be wrong at the boundary」. That fence was correct when written.
It is now discharged by measurement, ⛔ not by argument: objectui#9623 / PR objectui#9776 merged and put exactly that combination into production on a sibling surface — mayInvoke applied once to an action list in a renderer that also evaluates a visible predicate. Its at-tier contract review (record 5725416336, VERDICT PASS) measured both arms and found them preserved: unknown still fails OPEN, an EMPTY held set still gates normally, and the server remains the authority.
⇒ the two conventions do compose, on this codebase, today. ⚠️ That is a precedent to re-derive, ⛔ not to inherit — this bar's visible also ORs in can_override for the approvals decision set (objectui#3314 is in its own docblock), which objectui#9776's surface did not have.
Binding constraints, from triage 5713393173 — quoted, not paraphrased
「⛔ 这不是授权绕过。规范说的是「mirrored as a UI hide」—— 执行方是服务端。缺的是镜像,⛔ 不是执行。⇒ 修法是恢复不变量(具名不升级类),⛔ 不触人工地板的「安全/权限边界」条(那条管的是改动边界,不是补回镜像)。」
Clause-② reasoning — no this time, and the reason it changed
⚠️ On the sibling card objectui#9623 this seat declared yes conservatively and the at-tier review overturned it to no. That review measured the identical shape: filtering an action list through mayInvokenarrows a render set onto already-published contract text (requiredPermissions is 「mirrored as a UI hide」), adds no exported symbol, adds no key to a published payload, and relaxes no accept set.
⇒ this seat is no longer 拿不准, so the conservative yes is not the honest answer here — no, on that measurement rather than on preference. ⚠️ Still a declaration and ⛔ not a verdict: if the delivered diff adds an exported symbol, a new prop on a published interface, or touches a contract face, the declaration is wrong and the report must say so. The enqueue gate reads the real diff.
{
"issue": 9572,
"status": "done",
"branch": "claude/issue-9572-declared-actions-bar-capability-gate",
"pr": "https://github.com/objectstack-ai/objectui/pull/9805",
"session": "session_018HrVaotisyhgmot9o2MLRq",
"premise_still_valid": true,
"summary": "DeclaredActionsBar now mirrors ADR-0066 D4 requiredPermissions as a UI hide. The shared useCapabilityGate is applied ONCE to the located action set, inside the bar's own ActionProvider (a new module-private DeclaredActionsToolbar), which also owns the chrome decision so a wholly denied set draws nothing instead of an orphan divider. PLACEMENT is the repair: the hook reads the nearest provider ABOVE its caller and this bar is self-contained, so a gate in the outer body would read the host's provider or none at all and fail open forever; the new suite supplies the held set only through the bar's own provider so the outer placement cannot pass it. No enforcement moves into the UI, the fail-open-on-unknown doctrine is untouched, and because the gate is ANDed in front of the existing fail-CLOSED visible CEL the composition is monotone -- it can only hide more, never expose more. can_override (objectui#5178) is orthogonal and unchanged: it is a per-record viewer flag an action's own visible reads, requiredPermissions is a per-caller capability list; pinned both ways. Issue body and both comments read in full, no sanitizer truncation found (body and both comments end with intact footers). Newest Claim comment 5725743013 names THIS branch. assignee os-tesla was already set by the PM; I wrote none.",
"premise_checks": {
"zone2_1_four_probes": "PARTIALLY FALSIFIED, substance intact. Re-derived with `git grep -nw` on the bar: useCapabilityGate 0, mayInvoke 0, requiredPermissions 0 -- but getActionsForLocation has 1 hit, in a prose comment, NOT a call. Lit control on the same file: useCondition 4 hits. Lit control on the sibling: packages/components/src/renderers/action/action-bar.tsx imports useCapabilityGate and calls it (3/5/2/2 hits for the four probes) -- the instrument fires.",
"zone2_2_what_it_filters": "CONFIRMED before editing: locations (+ the exclude set), the visible CEL via useCondition(toPredicateInput(action.visible), ..., {throwOnError:true}) which is fail-CLOSED, and spec disabled via the same entry.",
"zone2_3_precedent_and_can_override": "CONFIRMED and re-derived, not inherited. data-table.tsx on origin/main calls useCapabilityGate once and filters schema.rowActionDefs before planDataTableRowMenu. Its own comment states the decisive rule -- it gates 'off the SAME ActionProvider this hook reads'. That is exactly what forced the placement here, because THIS bar mounts its own provider and the precedent surface did not. can_override: measured orthogonal, no fork. CORRECTION to the card: objectui#3314 in this file's docblock is about two toPredicateInput normalizations drifting, not about the can_override arm; the can_override history is objectui#5178 (affordance) and objectui#5414 (approver names). Both read before editing.",
"zone2_4_reachability": "MEASURED, and partly unmeasurable from this repo -- stated rather than guessed. Reachable as a PUBLISHED API: exported from packages/app-shell/src/index.ts and views/index.ts, package publishes dist, public prop actions?: ActionDef[] whose element type declares requiredPermissions (typed member off SpecActionInput, no cast needed). In-repo it is mounted only by apps/console ApprovalsInboxPage, twice, objectName sys_approval_request at location record_section, passing NO actions prop -- so the drawn set is whatever the backend declares, which is framework metadata this repository cannot read. Whether a capability-gated action reaches the console host TODAY is therefore a server-side fact, not an objectui one. This bears on grading, not on the correctness of the repair."
},
"tests": "RED BEFORE: pnpm exec vitest run packages/app-shell/src/views/__tests__/DeclaredActionsBar.capabilityGate-9572.test.tsx -> exit 1, 'Tests 5 failed | 6 passed (11)'. The 5 reds are the four hide arms + the orphan-chrome arm; the 6 greens are the guards that must stay green (capability held, fail-OPEN on absent systemPermissions, fail-OPEN with no user at all, chrome kept when one survives, can_override affordance preserved, visible-hidden stays hidden). GREEN AFTER: same command exit 0, 'Tests 11 passed (11)'. ABLATION (placement): gate moved to the bar's outer body, everything else identical; same 5 tests fail, same 6 guards green. Mutation proved ON DISK before the run -- injected marker grep -c = 2, removed text grep -c = 0, git hash-object d3ff3a54... != HEAD blob 2d2f0f0c...; restored via `git checkout HEAD -- path` under a trap and verified byte-for-byte afterwards (git diff HEAD empty, restored blob 2d2f0f0c... == HEAD blob, 0 marker residue). No rebuild sits between mutation and measurement: the root vitest config aliases @object-ui/* to each package's src. Direction predicted and observed: turns RED. No permanent test artifact left. FINAL CLEAN RUN at 998c8fd, after the tree was restored and committed: `pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run packages/app-shell/` under the shared verify lock -> VERDICT command-exit 0; 'Test Files 713 passed (713)', 'Tests 7017 passed | 1 skipped (7018)'. An earlier full-suite run overlapped the ablation window by ~8s and is therefore DISCARDED as contaminated rather than reported; the figure above is from the clean re-run.",
"gates": {
"prerequisite_build": "turbo run build --filter '@object-ui/app-shell^...' --concurrency=2 -> exit 0, 28/28 tasks. Needed first: without it type-check reports TS2307 on every workspace import, which is a missing prerequisite, not a red gate.",
"app-shell type-check (tsc --noEmit + tsc -p tsconfig.test.json)": "exit 0 (covers the new test file)",
"pnpm exec vitest run packages/app-shell/": "exit 0 -- 713 files, 7017 passed, 1 skipped",
"node scripts/check-changeset-presence.mjs": "exit 0 -- 1 changeset declared",
"node scripts/check-changeset-no-major.mjs": "exit 0",
"node scripts/check-changeset-overwrite.mjs": "exit 0",
"node scripts/check-control-bytes.mjs": "exit 0",
"node scripts/check-vi-mock-specifiers.mjs": "exit 0",
"node scripts/check-vi-mock-inherit.mjs": "exit 0",
"node scripts/check-vi-mock-override-shape.mjs": "exit 0",
"node scripts/check-test-path-roots.mjs": "exit 0",
"node scripts/check-new-cross-file-line-citations.mjs --base origin/main": "exit 0 -- 0 citations added by this branch",
"governed-surface membership": "NOT GOVERNED -- 0 of 5 GOVERNED_SURFACES matched (evaluated via governedPathsIn on this diff's paths)",
"lint": "DECLARED NARROWING, measured at 998c8fd, 3/3 evidence present: (1) population = root eslint.config.js files ['**/*.{ts,tsx}'], packages/app-shell has no config of its own; (2) file count read from --format json = 2 files linted, 0 errors; (3) type-aware linting NOT enabled -- no parserOptions.project, no projectService -- so no rule reads another file's types and this diff cannot move the verdict on any file it does not contain; the diff also changes no eslint config and adds no inline disable. Modified source file's warning profile is byte-identical to origin/main's (13 no-explicit-any + 1 react-hooks/exhaustive-deps, measured against the base revision of the same file): zero warnings added. New test file: 3 no-explicit-any, all inside test doubles copied verbatim from the two sibling DeclaredActionsBar suites. Repo-wide `pnpm lint` (turbo, every package) is CI's run.",
"NOT MEASURED": "CI convergence on PR #9805 -- no CI verdict was awaited, per the dispatch contract. Every local gate above has a real exit code captured to a file before it was read; none was read through a pipe."
},
"line_budget": "n/a -- this diff touches no `skills/**` path, so the published-skills line ratchet does not apply. Diff size vs BASE 3e4f632: 3 files, +399 / -20.",
"deviations": [
"ZONE 3 suggested applying the gate 'before the bar plans what to render', which reads as the bar's outer body. Followed in SPIRIT (once, over the list, one filtered source for chrome and items) but NOT in that literal position: measurement showed the outer body is above this bar's own ActionProvider, so useCapabilityGate would read the host's provider -- or none in a standalone host -- and fail open on every action forever. A new module-private DeclaredActionsToolbar carries the gate under the provider instead. The placement is pinned, not left to review.",
"Lint was narrowed rather than run repo-wide; declared above with all three required pieces of evidence.",
"No label write was made. The dispatch budget named exactly the report comment, the PR create and the PR body; no label was named for this card, and `skip-changeset` must never be applied in objectui. The labeler workflow applied `tests` and `package: app-shell` on its own.",
"PR body read-back shows GitHub APPENDED a second session-URL attribution footer block to the body I sent (sent 9431 bytes, read back 9521; the only delta is an added `---` + `_Generated by ...session_..._` block). Content otherwise byte-identical: first line `Fixes #9572` intact, no HTML comments, no tag-shaped fragments, no truncation. Not corrected -- dev writes the PR body once and does not PATCH."
],
"files_changed": [
"packages/app-shell/src/views/DeclaredActionsBar.tsx (modified)",
"packages/app-shell/src/views/__tests__/DeclaredActionsBar.capabilityGate-9572.test.tsx (new)",
".changeset/9572-declared-actions-bar-capability-mirror.md (new, patch bump for @object-ui/app-shell)"
],
"clause_2": "no -- and the delivered diff matches the declaration: no exported symbol added (DeclaredActionsToolbar is module-private), no member added to DeclaredActionsBarProps or any published interface, no contract face touched. The new component's props are Pick-ed from the existing DeclaredActionsBarProps rather than restated.",
"mcp_calls": "0 -- no MCP tool of any kind was called, GitHub or otherwise.",
"api_writes": "2 -- POST /repos/objectstack-ai/objectui/pulls (draft, body included in the same call); POST /repos/objectstack-ai/objectui/issues/9572/comments (this report). Reads, not counted as writes: GET /issues/9572, GET /issues/9572/comments, GET /pulls/9805.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the card's probe table reports all four probes at 0 hits on this file; re-derived, three are 0 and getActionsForLocation has exactly one hit, in a prose comment rather than a call. Card-accuracy nit, not a code defect; substance of the finding unchanged. Carrier: the reader of PR #9805, where it is written down.",
"noted, not filed: the card attributes the can_override OR to objectui#3314 'referenced in its own docblock'; that reference is about toPredicateInput normalization drift. The can_override arm is objectui#5178 / #5414. Same class. Carrier: the reader of PR #9805.",
"noted, not filed: useCapabilityGate has no variant taking the held set explicitly, so any surface filtering its own action list must be rendered under a provider that carries it -- the ambient-context foot-gun this change had to step over. Measured across the tree: DeclaredActionsBar is the ONLY file that both mounts an ActionProvider and calls the hook (lit control: ObjectView and RecordDetailView mount one and never call it; data-table.tsx calls it and only names the provider in prose). No sibling carries the hazard, so there is nothing to file. Carrier: the docblock now states the rule in the file where the next reader meets it.",
"noted, not filed: reachability of a capability-gated action on the in-repo host is a server-metadata fact this repository cannot read (see premise_checks.zone2_4_reachability). Not a defect and not a card. Carrier: none -- it is a grading input for the PM seat, delivered in this report and in PR #9805."
]
}
Reviewed by the dispatching domain:ui#2 seat, session_018HrVaotisyhgmot9o2MLRq, R1. Verified against GitHub and against the tree, ⛔ not against the report's narrative. ⛔ Not landed yet — CI has not converged; hold at the end.
⭐ The finding that makes this delivery worth reading: PLACEMENT is the repair
The suggested route said to gate 「before the bar plans what to render」, which reads as the bar's outer body. The dev measured that this would have been wrong and silently so: useCapabilityGate reads the nearest ActionProviderabove its caller, and this bar mounts its own provider. ⇒ a gate in the outer body would read the HOST's provider — or none at all in a standalone host — and fail open on every action, forever. A green suite and a rendered button would both have looked correct.
⇒ the gate lives in a new module-private DeclaredActionsToolbarunder the provider, and the placement is pinned, ⛔ not left to review: the new suite supplies the held set only through the bar's own provider, so the outer placement cannot pass. The ablation moves the gate to the outer body, everything else identical, and the same 5 tests red while the same 6 guards stay green.
⭐ This is the same rule the precedent states about itself — data-table.tsx gates 「off the SAME ActionProvider this hook reads」 — but the precedent surface did not mount its own provider, so the rule only bites here. ⇒ ⛔ the precedent was not copyable as written, and the dev found that rather than inheriting it.
Clause ② — in-seat, verified by this seat
probe on head 998c8fd6f2
reading
SUBJECT — DeclaredActionsToolbar exported?
0 hits
⭐ lit control — ^export lines in that file
3 ⇒ the zero is about the symbol, ⛔ not a dead file
⭐ control 2 — named in either barrel (src/index.ts, views/index.ts)?
0 hits
⇒ no confirmed: no exported symbol, no member added to DeclaredActionsBarProps or any published interface (the new component Picks from the existing props rather than restating them), no contract face touched. Neither enqueue limb fires ⇒ the review is this seat's at its own tier plus the gates. --pair 9805 ⇒ exit 0.
The binding constraints held
⛔ Enforcement did not move into the UI, and the fail-open-on-unknown doctrine is untouched — three of the six green guards exist precisely to hold that (capability held ⇒ shown; absent systemPermissions ⇒ shown; no user at all ⇒ shown). ⭐ And the composition argument is the right one: the gate is ANDed in front of the existing fail-CLOSED visible CEL, so it is monotone — it can only hide more, ⛔ never expose more. That is what makes 「mirror, not enforcement」 checkable rather than a slogan.
Two card corrections the delivery measured, both accepted
⚠️ The card's probe table says all four probes read 0 on this file. Re-derived: three are 0, but getActionsForLocation has exactly one hit — in a prose comment, ⛔ not a call. Substance unchanged; the count was wrong.
⚠️ The card attributes the can_override OR to objectui#3314 「referenced in its own docblock」. Measured: that reference is about two toPredicateInput normalisations drifting. The can_override history is objectui#5178 (affordance) and objectui#5414 (approver names). Both were read before editing, and can_override was measured orthogonal — a per-record viewer flag an action's own visible reads, versus a per-caller capability list — and pinned both ways. ⇒ ⛔ the fork the card feared does not exist.
⭐ Recorded because I passed both of those to the dev as premises: the card was wrong on the citation and the count, the dev checked rather than inherited, and the conclusion survived.
Reachability — measured, and honestly partly unmeasurable
Reachable as a published API (exported from src/index.ts and views/index.ts, package publishes dist, public actions?: ActionDef[] whose element type declares requiredPermissions). In-repo it is mounted only by apps/console's ApprovalsInboxPage, twice, passing noactions prop — so the drawn set is backend-declared metadata this repository cannot read. ⇒ whether a capability-gated action reaches the console host TODAY is a server-side fact, ⛔ not an objectui one. That bears on grading, ⛔ not on the correctness of the repair, and the dev said so rather than guessing.
⭐ A platform reading this seat CONFIRMED independently
The dev reported that GitHub appended a second attribution footer to the PR body it sent (9431 sent, 9521 read back, the delta being one extra footer block). I read the stored body myself: 2 footer blocks. ⇒ the double-footer condition is live over REST on a POST /pulls create, not only over the MCP body-PATCH channel this lane previously implicated. ⛔ The dev correctly did NOT patch the body to fix it — a dev writes the body once. Content otherwise byte-identical, Fixes #9572 intact, no truncation.
Evidence discipline worth copying
⭐ An earlier full-suite run overlapped the ablation window by ~8 seconds and was DISCARDED as contaminated rather than reported. The 713 files / 7017 tests figure is from a clean re-run at the committed head. That is the honest handling of a measurement whose instrument was disturbed, and ⛔ it would have been invisible to me if the dev had not said so.
⛔ Landing hold
CI at this write: 23 success · 3 skipped · 10 in_progress · 0 red. in_progress is an honest reading and ⛔ not a pass. Path surface against the five governed paths: none matched ⇒ ordinary merge-queue path. ⇒ this seat lands it on the next cycle once the shards, Lint and Type Check converge.
useCondition ⇒ still 4 ⇒ the instrument reads that file
⭐ CONTROL 2 — still module-private
no export of DeclaredActionsToolbar anywhere under packages/app-shell/src/** — grep exit 1, a genuine zero, ⛔ not a failed command posing as one
State cleaned explicitly
⚠️Fixes closed the card and left pm:dispatched and the assignee standing — lane fact ②, now 37 for 37. Cleaned in one stroke.
⭐ What this landing is really worth: PLACEMENT, and it is now recorded in the source
The suggested route said to gate 「before the bar plans what to render」, i.e. the outer body. That would have been wrong and silently so.useCapabilityGate reads the nearest ActionProviderabove its caller, and this bar mounts its own provider — so an outer-body gate reads the HOST's provider, or none at all in a standalone host, and fails open on every action forever, with a green suite and a rendered button.
⇒ the gate sits in a module-private DeclaredActionsToolbarunder the provider, and the placement is pinned: the suite supplies the held set only through the bar's own provider, so the outer placement cannot pass. The ablation moves the gate out, everything else identical, and the same 5 cases red while the same 6 guards stay green. ⭐ origin/main:443 now carries 「PLACEMENT is load-bearing」 in the source, where the next reader meets it.
⭐ The precedent stated this rule about itself (objectui#9776's data-table.tsx gates 「off the SAME ActionProvider this hook reads」) but could not demonstrate it, because that surface mounts no provider of its own. ⇒ the precedent was ⛔ not copyable as written, and the dev measured that rather than inheriting it.
Two card corrections, measured by the delivery
⚠️ The card's probe table claims all four probes read 0. Re-derived: three are 0, but getActionsForLocation has one hit — in a prose comment, ⛔ not a call.
⚠️ The card attributes the can_override OR to objectui#3314. Measured: that reference is about toPredicateInput normalisation drift; the can_override history is objectui#5178 / objectui#5414. can_override was then measured orthogonal to requiredPermissions — a per-record viewer flag versus a per-caller capability list — and pinned both ways. ⇒ ⛔ the fork the card feared does not exist.
⭐ Both were premises this seat passed down. The dev checked rather than inherited, and the conclusion survived while the reasons did not.
The family, now two-thirds closed
surface
state
the grid ROW menu
objectui#9623 · MERGED
the declared-actions bar
this card · MERGED
the related-list TOOLBAR
objectui#9782 · dispatched, dev out
⇒ one declared contract (requiredPermissions 「mirrored as a UI hide」), three carriers, two paid.
⛔ A grading input, ⛔ not a defect
Reachability is partly unmeasurable from this repository: the bar is a published API, but its only in-repo mount (apps/console's ApprovalsInboxPage) passes no actions prop, so the drawn set is backend-declared metadata this repo cannot read. The dev said so rather than guessing.
domain:ui#2 execution seat · session_018HrVaotisyhgmot9o2MLRq · landing record · readings taken 2026-09-18T07:24Z
Activity
os-tesla commentedon Sep 18, 2026
Claim: PM loop round 1⚠️ objectui#9785 touches
Session:
session_018HrVaotisyhgmot9o2MLRqBranch:
claude/issue-9572-declared-actions-bar-capability-gateWorktree:
objectui-issue-9572Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/DeclaredActionsBar.tsxplus its test file — stop on breach; explain in the reportContainer & model:
M,mode:subagent,model: default judgment tierClause-②: no
Thread-read: 5713393173
Serial constraints cleared: no in-flight
domain:uicard declares this file. This seat's other cards are all delivered and in landing: objectui#9623 (PR objectui#9776, MERGED), objectui#9580 (PR objectui#9783, MERGED), objectui#8167 (PR objectui#9785, armed), objectui#9691 (PR objectui#9788, queued), objectui#9675 (PR objectui#9790, armed), objectui#9710 (PR objectui#9796, in review).app-shell/src/views/metadata-admin/; this card isapp-shell/src/views/DeclaredActionsBar.tsx— different file, and the independence test is the file surface, ⛔ not the package and ⛔ not the directory. Seat 1 holds objectui#9382 / objectui#9452 / objectui#9453 and objectui#9570 / objectui#9373 / objectui#9061 / objectui#7945, none in this file.⭐ The boundary question this card raised is now answered by a LANDED precedent
The card fenced itself with a real warning: 「the gate's doctrine is fail-open on unknown … and the bar's
visiblepredicate is deliberately fail-closed, so adding a gate changes behaviour in a direction the two conventions do not agree on by default … a fix that simply dropsmayInvokein besidevisiblemay be wrong at the boundary」. That fence was correct when written.It is now discharged by measurement, ⛔ not by argument: objectui#9623 / PR objectui#9776 merged and put exactly that combination into production on a sibling surface —
mayInvokeapplied once to an action list in a renderer that also evaluates avisiblepredicate. Its at-tier contract review (record5725416336, VERDICT PASS) measured both arms and found them preserved: unknown still fails OPEN, an EMPTY held set still gates normally, and the server remains the authority.⇒ the two conventions do compose, on this codebase, today.⚠️ That is a precedent to re-derive, ⛔ not to inherit — this bar's
visiblealso ORs incan_overridefor the approvals decision set (objectui#3314 is in its own docblock), which objectui#9776's surface did not have.Binding constraints, from triage
5713393173— quoted, not paraphrasedClause-② reasoning —
nothis time, and the reason it changedyesconservatively and the at-tier review overturned it tono. That review measured the identical shape: filtering an action list throughmayInvokenarrows a render set onto already-published contract text (requiredPermissionsis 「mirrored as a UI hide」), adds no exported symbol, adds no key to a published payload, and relaxes no accept set.⇒ this seat is no longer 拿不准, so the conservative⚠️ Still a declaration and ⛔ not a verdict: if the delivered diff adds an exported symbol, a new prop on a published interface, or touches a contract face, the declaration is wrong and the report must say so. The enqueue gate reads the real diff.
yesis not the honest answer here —no, on that measurement rather than on preference.domain:ui#2execution seat ·session_018HrVaotisyhgmot9o2MLRq· claim · reading taken 2026-09-18T05:45ZGenerated by Claude Code
os-tesla commentedon Sep 18, 2026
os-dev-report
{ "issue": 9572, "status": "done", "branch": "claude/issue-9572-declared-actions-bar-capability-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/9805", "session": "session_018HrVaotisyhgmot9o2MLRq", "premise_still_valid": true, "summary": "DeclaredActionsBar now mirrors ADR-0066 D4 requiredPermissions as a UI hide. The shared useCapabilityGate is applied ONCE to the located action set, inside the bar's own ActionProvider (a new module-private DeclaredActionsToolbar), which also owns the chrome decision so a wholly denied set draws nothing instead of an orphan divider. PLACEMENT is the repair: the hook reads the nearest provider ABOVE its caller and this bar is self-contained, so a gate in the outer body would read the host's provider or none at all and fail open forever; the new suite supplies the held set only through the bar's own provider so the outer placement cannot pass it. No enforcement moves into the UI, the fail-open-on-unknown doctrine is untouched, and because the gate is ANDed in front of the existing fail-CLOSED visible CEL the composition is monotone -- it can only hide more, never expose more. can_override (objectui#5178) is orthogonal and unchanged: it is a per-record viewer flag an action's own visible reads, requiredPermissions is a per-caller capability list; pinned both ways. Issue body and both comments read in full, no sanitizer truncation found (body and both comments end with intact footers). Newest Claim comment 5725743013 names THIS branch. assignee os-tesla was already set by the PM; I wrote none.", "premise_checks": { "zone2_1_four_probes": "PARTIALLY FALSIFIED, substance intact. Re-derived with `git grep -nw` on the bar: useCapabilityGate 0, mayInvoke 0, requiredPermissions 0 -- but getActionsForLocation has 1 hit, in a prose comment, NOT a call. Lit control on the same file: useCondition 4 hits. Lit control on the sibling: packages/components/src/renderers/action/action-bar.tsx imports useCapabilityGate and calls it (3/5/2/2 hits for the four probes) -- the instrument fires.", "zone2_2_what_it_filters": "CONFIRMED before editing: locations (+ the exclude set), the visible CEL via useCondition(toPredicateInput(action.visible), ..., {throwOnError:true}) which is fail-CLOSED, and spec disabled via the same entry.", "zone2_3_precedent_and_can_override": "CONFIRMED and re-derived, not inherited. data-table.tsx on origin/main calls useCapabilityGate once and filters schema.rowActionDefs before planDataTableRowMenu. Its own comment states the decisive rule -- it gates 'off the SAME ActionProvider this hook reads'. That is exactly what forced the placement here, because THIS bar mounts its own provider and the precedent surface did not. can_override: measured orthogonal, no fork. CORRECTION to the card: objectui#3314 in this file's docblock is about two toPredicateInput normalizations drifting, not about the can_override arm; the can_override history is objectui#5178 (affordance) and objectui#5414 (approver names). Both read before editing.", "zone2_4_reachability": "MEASURED, and partly unmeasurable from this repo -- stated rather than guessed. Reachable as a PUBLISHED API: exported from packages/app-shell/src/index.ts and views/index.ts, package publishes dist, public prop actions?: ActionDef[] whose element type declares requiredPermissions (typed member off SpecActionInput, no cast needed). In-repo it is mounted only by apps/console ApprovalsInboxPage, twice, objectName sys_approval_request at location record_section, passing NO actions prop -- so the drawn set is whatever the backend declares, which is framework metadata this repository cannot read. Whether a capability-gated action reaches the console host TODAY is therefore a server-side fact, not an objectui one. This bears on grading, not on the correctness of the repair." }, "tests": "RED BEFORE: pnpm exec vitest run packages/app-shell/src/views/__tests__/DeclaredActionsBar.capabilityGate-9572.test.tsx -> exit 1, 'Tests 5 failed | 6 passed (11)'. The 5 reds are the four hide arms + the orphan-chrome arm; the 6 greens are the guards that must stay green (capability held, fail-OPEN on absent systemPermissions, fail-OPEN with no user at all, chrome kept when one survives, can_override affordance preserved, visible-hidden stays hidden). GREEN AFTER: same command exit 0, 'Tests 11 passed (11)'. ABLATION (placement): gate moved to the bar's outer body, everything else identical; same 5 tests fail, same 6 guards green. Mutation proved ON DISK before the run -- injected marker grep -c = 2, removed text grep -c = 0, git hash-object d3ff3a54... != HEAD blob 2d2f0f0c...; restored via `git checkout HEAD -- path` under a trap and verified byte-for-byte afterwards (git diff HEAD empty, restored blob 2d2f0f0c... == HEAD blob, 0 marker residue). No rebuild sits between mutation and measurement: the root vitest config aliases @object-ui/* to each package's src. Direction predicted and observed: turns RED. No permanent test artifact left. FINAL CLEAN RUN at 998c8fd, after the tree was restored and committed: `pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run packages/app-shell/` under the shared verify lock -> VERDICT command-exit 0; 'Test Files 713 passed (713)', 'Tests 7017 passed | 1 skipped (7018)'. An earlier full-suite run overlapped the ablation window by ~8s and is therefore DISCARDED as contaminated rather than reported; the figure above is from the clean re-run.", "gates": { "prerequisite_build": "turbo run build --filter '@object-ui/app-shell^...' --concurrency=2 -> exit 0, 28/28 tasks. Needed first: without it type-check reports TS2307 on every workspace import, which is a missing prerequisite, not a red gate.", "app-shell type-check (tsc --noEmit + tsc -p tsconfig.test.json)": "exit 0 (covers the new test file)", "pnpm exec vitest run packages/app-shell/": "exit 0 -- 713 files, 7017 passed, 1 skipped", "node scripts/check-changeset-presence.mjs": "exit 0 -- 1 changeset declared", "node scripts/check-changeset-no-major.mjs": "exit 0", "node scripts/check-changeset-overwrite.mjs": "exit 0", "node scripts/check-control-bytes.mjs": "exit 0", "node scripts/check-vi-mock-specifiers.mjs": "exit 0", "node scripts/check-vi-mock-inherit.mjs": "exit 0", "node scripts/check-vi-mock-override-shape.mjs": "exit 0", "node scripts/check-test-path-roots.mjs": "exit 0", "node scripts/check-new-cross-file-line-citations.mjs --base origin/main": "exit 0 -- 0 citations added by this branch", "governed-surface membership": "NOT GOVERNED -- 0 of 5 GOVERNED_SURFACES matched (evaluated via governedPathsIn on this diff's paths)", "lint": "DECLARED NARROWING, measured at 998c8fd, 3/3 evidence present: (1) population = root eslint.config.js files ['**/*.{ts,tsx}'], packages/app-shell has no config of its own; (2) file count read from --format json = 2 files linted, 0 errors; (3) type-aware linting NOT enabled -- no parserOptions.project, no projectService -- so no rule reads another file's types and this diff cannot move the verdict on any file it does not contain; the diff also changes no eslint config and adds no inline disable. Modified source file's warning profile is byte-identical to origin/main's (13 no-explicit-any + 1 react-hooks/exhaustive-deps, measured against the base revision of the same file): zero warnings added. New test file: 3 no-explicit-any, all inside test doubles copied verbatim from the two sibling DeclaredActionsBar suites. Repo-wide `pnpm lint` (turbo, every package) is CI's run.", "NOT MEASURED": "CI convergence on PR #9805 -- no CI verdict was awaited, per the dispatch contract. Every local gate above has a real exit code captured to a file before it was read; none was read through a pipe." }, "line_budget": "n/a -- this diff touches no `skills/**` path, so the published-skills line ratchet does not apply. Diff size vs BASE 3e4f632: 3 files, +399 / -20.", "deviations": [ "ZONE 3 suggested applying the gate 'before the bar plans what to render', which reads as the bar's outer body. Followed in SPIRIT (once, over the list, one filtered source for chrome and items) but NOT in that literal position: measurement showed the outer body is above this bar's own ActionProvider, so useCapabilityGate would read the host's provider -- or none in a standalone host -- and fail open on every action forever. A new module-private DeclaredActionsToolbar carries the gate under the provider instead. The placement is pinned, not left to review.", "Lint was narrowed rather than run repo-wide; declared above with all three required pieces of evidence.", "No label write was made. The dispatch budget named exactly the report comment, the PR create and the PR body; no label was named for this card, and `skip-changeset` must never be applied in objectui. The labeler workflow applied `tests` and `package: app-shell` on its own.", "PR body read-back shows GitHub APPENDED a second session-URL attribution footer block to the body I sent (sent 9431 bytes, read back 9521; the only delta is an added `---` + `_Generated by ...session_..._` block). Content otherwise byte-identical: first line `Fixes #9572` intact, no HTML comments, no tag-shaped fragments, no truncation. Not corrected -- dev writes the PR body once and does not PATCH." ], "files_changed": [ "packages/app-shell/src/views/DeclaredActionsBar.tsx (modified)", "packages/app-shell/src/views/__tests__/DeclaredActionsBar.capabilityGate-9572.test.tsx (new)", ".changeset/9572-declared-actions-bar-capability-mirror.md (new, patch bump for @object-ui/app-shell)" ], "clause_2": "no -- and the delivered diff matches the declaration: no exported symbol added (DeclaredActionsToolbar is module-private), no member added to DeclaredActionsBarProps or any published interface, no contract face touched. The new component's props are Pick-ed from the existing DeclaredActionsBarProps rather than restated.", "mcp_calls": "0 -- no MCP tool of any kind was called, GitHub or otherwise.", "api_writes": "2 -- POST /repos/objectstack-ai/objectui/pulls (draft, body included in the same call); POST /repos/objectstack-ai/objectui/issues/9572/comments (this report). Reads, not counted as writes: GET /issues/9572, GET /issues/9572/comments, GET /pulls/9805.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the card's probe table reports all four probes at 0 hits on this file; re-derived, three are 0 and getActionsForLocation has exactly one hit, in a prose comment rather than a call. Card-accuracy nit, not a code defect; substance of the finding unchanged. Carrier: the reader of PR #9805, where it is written down.", "noted, not filed: the card attributes the can_override OR to objectui#3314 'referenced in its own docblock'; that reference is about toPredicateInput normalization drift. The can_override arm is objectui#5178 / #5414. Same class. Carrier: the reader of PR #9805.", "noted, not filed: useCapabilityGate has no variant taking the held set explicitly, so any surface filtering its own action list must be rendered under a provider that carries it -- the ambient-context foot-gun this change had to step over. Measured across the tree: DeclaredActionsBar is the ONLY file that both mounts an ActionProvider and calls the hook (lit control: ObjectView and RecordDetailView mount one and never call it; data-table.tsx calls it and only names the provider in prose). No sibling carries the hazard, so there is nothing to file. Carrier: the docblock now states the rule in the file where the next reader meets it.", "noted, not filed: reachability of a capability-gated action on the in-repo host is a server-metadata fact this repository cannot read (see premise_checks.zone2_4_reachability). Not a defect and not a card. Carrier: none -- it is a grading input for the PM seat, delivered in this report and in PR #9805." ] }Generated by Claude Code
os-tesla commentedon Sep 18, 2026
✅ ACCEPT — PR objectui#9805
Reviewed by the dispatching
domain:ui#2seat,session_018HrVaotisyhgmot9o2MLRq, R1. Verified against GitHub and against the tree, ⛔ not against the report's narrative. ⛔ Not landed yet — CI has not converged; hold at the end.⭐ The finding that makes this delivery worth reading: PLACEMENT is the repair
The suggested route said to gate 「before the bar plans what to render」, which reads as the bar's outer body. The dev measured that this would have been wrong and silently so:
useCapabilityGatereads the nearestActionProviderabove its caller, and this bar mounts its own provider. ⇒ a gate in the outer body would read the HOST's provider — or none at all in a standalone host — and fail open on every action, forever. A green suite and a rendered button would both have looked correct.⇒ the gate lives in a new module-private
DeclaredActionsToolbarunder the provider, and the placement is pinned, ⛔ not left to review: the new suite supplies the held set only through the bar's own provider, so the outer placement cannot pass. The ablation moves the gate to the outer body, everything else identical, and the same 5 tests red while the same 6 guards stay green.⭐ This is the same rule the precedent states about itself —
data-table.tsxgates 「off the SAME ActionProvider this hook reads」 — but the precedent surface did not mount its own provider, so the rule only bites here. ⇒ ⛔ the precedent was not copyable as written, and the dev found that rather than inheriting it.Clause ② — in-seat, verified by this seat
998c8fd6f2DeclaredActionsToolbarexported?^exportlines in that filesrc/index.ts,views/index.ts)?⇒
noconfirmed: no exported symbol, no member added toDeclaredActionsBarPropsor any published interface (the new component Picks from the existing props rather than restating them), no contract face touched. Neither enqueue limb fires ⇒ the review is this seat's at its own tier plus the gates.--pair 9805⇒ exit 0.The binding constraints held
⛔ Enforcement did not move into the UI, and the fail-open-on-unknown doctrine is untouched — three of the six green guards exist precisely to hold that (capability held ⇒ shown; absent
systemPermissions⇒ shown; no user at all ⇒ shown). ⭐ And the composition argument is the right one: the gate is ANDed in front of the existing fail-CLOSEDvisibleCEL, so it is monotone — it can only hide more, ⛔ never expose more. That is what makes 「mirror, not enforcement」 checkable rather than a slogan.Two card corrections the delivery measured, both accepted
getActionsForLocationhas exactly one hit — in a prose comment, ⛔ not a call. Substance unchanged; the count was wrong.can_overrideOR to objectui#3314 「referenced in its own docblock」. Measured: that reference is about twotoPredicateInputnormalisations drifting. Thecan_overridehistory is objectui#5178 (affordance) and objectui#5414 (approver names). Both were read before editing, andcan_overridewas measured orthogonal — a per-record viewer flag an action's ownvisiblereads, versus a per-caller capability list — and pinned both ways. ⇒ ⛔ the fork the card feared does not exist.⭐ Recorded because I passed both of those to the dev as premises: the card was wrong on the citation and the count, the dev checked rather than inherited, and the conclusion survived.
Reachability — measured, and honestly partly unmeasurable
Reachable as a published API (exported from
src/index.tsandviews/index.ts, package publishesdist, publicactions?: ActionDef[]whose element type declaresrequiredPermissions). In-repo it is mounted only byapps/console's ApprovalsInboxPage, twice, passing noactionsprop — so the drawn set is backend-declared metadata this repository cannot read. ⇒ whether a capability-gated action reaches the console host TODAY is a server-side fact, ⛔ not an objectui one. That bears on grading, ⛔ not on the correctness of the repair, and the dev said so rather than guessing.⭐ A platform reading this seat CONFIRMED independently
The dev reported that GitHub appended a second attribution footer to the PR body it sent (9431 sent, 9521 read back, the delta being one extra footer block). I read the stored body myself: 2 footer blocks. ⇒ the double-footer condition is live over REST on a
POST /pullscreate, not only over the MCP body-PATCH channel this lane previously implicated. ⛔ The dev correctly did NOT patch the body to fix it — a dev writes the body once. Content otherwise byte-identical,Fixes #9572intact, no truncation.Evidence discipline worth copying
⭐ An earlier full-suite run overlapped the ablation window by ~8 seconds and was DISCARDED as contaminated rather than reported. The 713 files / 7017 tests figure is from a clean re-run at the committed head. That is the honest handling of a measurement whose instrument was disturbed, and ⛔ it would have been invisible to me if the dev had not said so.
⛔ Landing hold
CI at this write: 23 success · 3 skipped · 10 in_progress · 0 red.
in_progressis an honest reading and ⛔ not a pass. Path surface against the five governed paths: none matched ⇒ ordinary merge-queue path. ⇒ this seat lands it on the next cycle once the shards, Lint and Type Check converge.domain:ui#2execution seat ·session_018HrVaotisyhgmot9o2MLRq· ACCEPT · readings taken 2026-09-18T06:38ZGenerated by Claude Code
os-tesla commentedon Sep 18, 2026
⭐ LANDED — PR objectui#9805 MERGED, verified BY CONTENT
domain:ui#2execution seat,session_018HrVaotisyhgmot9o2MLRq. Merged 2026-09-18T07:15:48Z.Verification — ⛔ not by sha, ⛔ not by an ancestor test
origin/mainDeclaredActionsBar.tsx:38importsuseCapabilityGate,:482calls it,:484isactions.filter((action) => mayInvoke(action.requiredPermissions))DeclaredActionsToolbar⇒ 3 linesuseCondition⇒ still 4 ⇒ the instrument reads that fileexportofDeclaredActionsToolbaranywhere underpackages/app-shell/src/**— grep exit 1, a genuine zero, ⛔ not a failed command posing as oneState cleaned explicitly
Fixesclosed the card and leftpm:dispatchedand the assignee standing — lane fact ②, now 37 for 37. Cleaned in one stroke.⭐ What this landing is really worth: PLACEMENT, and it is now recorded in the source
The suggested route said to gate 「before the bar plans what to render」, i.e. the outer body. That would have been wrong and silently so.
useCapabilityGatereads the nearestActionProviderabove its caller, and this bar mounts its own provider — so an outer-body gate reads the HOST's provider, or none at all in a standalone host, and fails open on every action forever, with a green suite and a rendered button.⇒ the gate sits in a module-private
DeclaredActionsToolbarunder the provider, and the placement is pinned: the suite supplies the held set only through the bar's own provider, so the outer placement cannot pass. The ablation moves the gate out, everything else identical, and the same 5 cases red while the same 6 guards stay green. ⭐origin/main:443now carries 「PLACEMENT is load-bearing」 in the source, where the next reader meets it.⭐ The precedent stated this rule about itself (objectui#9776's
data-table.tsxgates 「off the SAME ActionProvider this hook reads」) but could not demonstrate it, because that surface mounts no provider of its own. ⇒ the precedent was ⛔ not copyable as written, and the dev measured that rather than inheriting it.Two card corrections, measured by the delivery
getActionsForLocationhas one hit — in a prose comment, ⛔ not a call.can_overrideOR to objectui#3314. Measured: that reference is abouttoPredicateInputnormalisation drift; thecan_overridehistory is objectui#5178 / objectui#5414.can_overridewas then measured orthogonal torequiredPermissions— a per-record viewer flag versus a per-caller capability list — and pinned both ways. ⇒ ⛔ the fork the card feared does not exist.⭐ Both were premises this seat passed down. The dev checked rather than inherited, and the conclusion survived while the reasons did not.
The family, now two-thirds closed
⇒ one declared contract (
requiredPermissions「mirrored as a UI hide」), three carriers, two paid.⛔ A grading input, ⛔ not a defect
Reachability is partly unmeasurable from this repository: the bar is a published API, but its only in-repo mount (
apps/console's ApprovalsInboxPage) passes noactionsprop, so the drawn set is backend-declared metadata this repo cannot read. The dev said so rather than guessing.domain:ui#2execution seat ·session_018HrVaotisyhgmot9o2MLRq· landing record · readings taken 2026-09-18T07:24ZGenerated by Claude Code
fix(app-shell): `DeclaredActionsBar` mirrors the ADR-0066 D4 capabili…
fix(core/app-shell): accept and resolve an inline `I18nLabel` on an a…