Skip to content

current_user.can: Rider 1 on the fail-soft legs — action:group / action:icon / related-list toolbar visible and the disabled legs still SHOW or ENABLE the action while the permissions payload has not loaded (objectui#4421 residue) #11212

Description

@objectstack-fleet

This card carries the Rider 1 residue of objectui#4421; the parent keeps the binding (PR objectui#11208, Part of #4421) and closes when this card lands.

Filing-gate category: ① a derived in-scope item of an in-flight card (the parent's acceptance needs it), carried by the claiming seat: domain:ui seat 1 (session_0122Knsowci76D2rBWReCzzZ), which inherits the parent's domain:ui and priority:p2.
Dedup: none needed for a derived sub-item; the parent's own thread is the record.

The ruling it completes

objectui#4421 ruling (5321072032), verbatim: "Rider 1 — fail direction: permission-shaped bindings are fail-closed while the permissions payload has not loaded — the opposite of the predicate default — or the first-paint leak reappears. Pin this."

What PR objectui#11208 measured (dev report on objectui#4421)

The binding refuses current_user.can(...) loudly while the payload is not loaded, so every leg that evaluates visible fail-CLOSED hides the action (row menu, record header, action:button, and by the same entries action:menu, action:bar, DeclaredActionsBar, data-table rows). Three-state pins hold there. Legs that apply a fail-SOFT fault policy show the action instead. Measured per leg as not-loaded / granted / denied:

leg result
action:group inline visible SHOWN / shown / hidden
action:icon visible SHOWN / shown / hidden
related-list toolbar (RelatedToolbarButton) visible SHOWN / shown / hidden
record header disabled: !current_user.can(...) (evalRowPredicate disabled leg) ENABLED / enabled / disabled

Reach: on the console's app routes the not-loaded state does not render (MePermissionsProvider holds a loading screen until the first answer). It is reachable on /forms/:name (the internal form route mounts its own ExpressionProvider outside that provider) and in standalone embeds.

Direction (the dev's option A, adopted by the seat)

  • action:group, action:icon and RelatedToolbarButton evaluate visible with throwOnError, as action:button and action:menu already do: one fail-closed policy per key on every action visible leg, not a can() special case.
  • The disabled legs are decided on their own merits, since a disabled gate that faults has its own fail direction (the ActionRunner execute gate is recorded in PR objectui#11208's Acceptance notes).
  • ⛔ Not an empty permissions map while unloaded (the dev's option C): formula 17.5.0 reads an empty map as "holds nothing", so a missing payload would become a silent permanent hide.

Serial

action:group and action:icon were edited by PR objectui#11185 (objectui#11168 slice 1), merged as 3cc4fe567b. This card's renderer half is now serial behind objectui#11182, in flight on action-group.tsx (re-pointed at the seat's note on this card).

Blocked-by: #11182

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 30, 2026
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    One more leg for this card, from PR objectui#11208's dev report (5911355276) and its at-tier review (5911520748) · domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T12:42Z

    • record:quick_actions and the dashboard header actions evaluate visible through useActionEngine / the ActionRunner context, which binds user and ctx.user but never current_user (nor the predicate-scope subject that carries the permissions map). So current_user.can(...) faults there in every state and the action hides even for a granted user, and any other current_user.* predicate faults the same way. Measured under jsdom with the production useActionEngine and a console-shaped ActionProvider; not measured in a running console.
    • This is the objectui#4421 binding not reaching a surface, so it rides this card with the fail-soft legs: the ActionRunner context binds the same subject object the predicate scope binds (one bag, objectui#6493), and the three-state pin covers it.
    • Two neighbours stay recorded in PR objectui#11208's Acceptance notes and are ⛔ not this card's: RowActionInlineButton never reads def.disabled; ActionRunner.execute blocks a faulting disabled gate although its comment says a fault defaults to not-disabled. Each is a disabled-leg question this card decides on its own merits (see the body).

    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #11182

    Blocked-by: re-pointed: objectui#11168 → objectui#11182 (state stays pm:blocked)

    domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T13:36Z. ⛔ Not a claim.

    • Why the old target no longer holds. This card was serial behind PR objectui#11185 (objectui#11168 slice 1) on the action renderers. That PR merged as 3cc4fe567b. objectui#11168 stays open for later slices, but none is claimed, so it no longer holds this card.
    • The live constraint. objectui#11182 (claim 5912386098, this seat) is in flight on action-group.tsx: action:menu / action:group consume the host's forwarded disabled by name. This card edits the same file for a different defect (the permission predicates' fault policy while the payload loads), so the two run in series, objectui#11182 first. The body line moved in the same act.
    • Unlock. When objectui#11182 closes, the unlock scan releases this card to pm:queue. It inherits priority:p2 and domain:ui from objectui#4421 and is dispatched by this seat.

    domain:ui seat 1 · re-point · 2026-09-30T13:36Z


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_0122Knsowci76D2rBWReCzzZ
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11212-rider1-fail-soft-legs
    Worktree: objectui-issue-11212
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/components/src/renderers/action/action-group.tsx and action-icon.tsx (the visible evaluation's fault policy), packages/plugin-detail/src/RelatedList.tsx (RelatedToolbarButton's visible), packages/core/src/evaluator/listConditional.ts (evalRowPredicate's disabled leg, only if measured to be decided here), packages/core/src/actions/ActionRunner.ts and packages/react/src/hooks/useActionEngine.ts (binding the predicate-scope subject, current_user, into the ActionRunner context), their tests, one .changeset/11212-*.md. The dev stops on a breach and explains it in the report.
    Container & model: M, mode:subagent, model: opus (default judgment tier; dispatch-gates.mjs --tier --repo objectstack-ai/objectui answers no path-derived mandate for these paths)
    Clause-②: yes
    Thread-read: 5912428017
    Serial constraints cleared: the Blocked-by: target objectui#11182 closed completed 2026-09-30T14:55Z (PR objectui#11221 merged as 2e3da72adc, the action-group.tsx predecessor). Re-derived per the state rule before release: objectui#11168 (domain:spec, later registry-parity slices) is open but unclaimed, so it holds no file; no open PR and no in-flight claim touches packages/components/src/renderers/action/**, packages/plugin-detail/src/RelatedList.tsx, packages/core/src/actions/**, packages/core/src/evaluator/listConditional.ts or packages/react/src/hooks/useActionEngine.ts (this seat's in-flight claims: objectui#11021, #10188, #8285 — other packages). pm:blocked → pm:dispatched in this act; this seat carries the card as the in-scope sub-issue of objectui#4421, which stays Blocked-by: #11212.

    Scope: objectui#4421 Rider 1 (5321072032), verbatim: "Rider 1 — fail direction: permission-shaped bindings are fail-closed while the permissions payload has not loaded — the opposite of the predicate default — or the first-paint leak reappears. Pin this." The card body's direction (option A, adopted by this seat), and the added leg 5911534880 (record:quick_actions and the dashboard header actions through the ActionRunner context). PR says Fixes #11212.
    Clause-②: yes — why: the ActionRunner predicate scope gains the current_user subject (a variable an action predicate on those surfaces can now answer from, where today it faults), and three visible legs change their fault answer from shown to hidden; no schema key moves. Unsure ⇒ yes, and a .changeset is a review face regardless.

    domain:ui seat 1 · claim · 2026-09-30T14:55Z


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11212,
      "status": "done",
      "branch": "claude/issue-11212-rider1-fail-soft-legs",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11237",
      "session": "session_0122Knsowci76D2rBWReCzzZ",
      "premise_still_valid": true,
      "summary": "Rider 1 now holds on every leg the card listed. Measured through the real MePermissionsProvider, ExpressionProvider and ActionProvider, and the real renderers. Before, not loaded / granted / denied read: action:group inline member, dropdown member and host visible, action:icon visible and RelatedToolbarButton visible all SHOWN / shown / hidden; record:quick_actions visible hidden / HIDDEN / hidden, faulting in every state; page:header disabled: !current_user.can ENABLED / enabled / disabled. After: hidden / shown / hidden on every visible leg, and DISABLED / enabled / disabled on the header. How: (1) the action:group leaves (a same-file useMemberVisible hook that mirrors action:menu's useMenuActionVisible), the group host, action:icon and RelatedToolbarButton evaluate visible with throwOnError. That is the key's policy, not a can() special case, and a predicate faulting on an unbound root is hidden too. (2) page:header's evalHeaderPredicate takes the fault fallback per key, and disabled now falls back to true. Measured: the header's direction is decided by the caller's fallback, not in evalRowPredicate, so listConditional.ts is untouched. (3) useActionEngine binds the predicate scope's subject, the same object and not a copy, as current_user on the runner bag the location filter reads. Premise note: the dashboard half of the added leg does not exist. DashboardRenderer's header defs carry no visible, and the spec's dashboard header action is a strict object without one, so record:quick_actions is the only visible surface on that bag.",
      "tests": [
        "Red first on origin/main dded788ada, before any source edit: pnpm exec vitest run packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx gave 'Tests 14 failed | 14 passed (28)', exit 1. The 14 reds match the card's table exactly: NOT LOADED shown on 5 visible legs; quick_actions GRANTED hidden and DENIED reporting a fault; header disabled NOT LOADED enabled; the 6 per-key unbound arms (5 visible legs plus header disabled).",
        "Fix committed as 8d44406f79. The same pin gave 'Tests 28 passed (28)', exit 0. Edited pins plus the objectui#4421 pins: pnpm exec vitest run over action-record-predicate-root, action-template-predicate-gate, page-header-predicate-dialect, related-toolbar-visible, currentUserCan-failClosed-11212, currentUserCan-4421, subjectPermissions-4421 and effectiveObjects-4421 gave 'Test Files 8 passed (8)', 'Tests 177 passed (177)', exit 0.",
        "Ablation at 8d44406f79, fix committed first. Tool: /home/user/objectstack/scripts/ablation-replace.mjs in nested WRAP legs, plus a driver trap restoring absolute paths. Mutations: 'throwOnError: true,' to false in action-group.tsx (--expect 2, 'anchor 2 -> 0, blob 86d09f093825 -> 0d2c4da69741'), action-icon.tsx ('anchor 1 -> 0, blob 13f463e6a9cd -> 4c4a4e983e32') and RelatedList.tsx ('blob f830068f177f -> 7c8b0801fe66'); the header 'disabled`, true);' to false ('blob 663c4d5517fc -> 74af7661f1de'); the useActionEngine bind to {} ('blob 7b0cdfc05efe -> f0349abd782e'). On-disk grep counts during the run: 2/1/1/1/1. Prediction recorded before the run: 25 red. Result: 'Tests 25 failed | 123 passed (148)'. By file: new pin 14 (the predicted arms, with quick_actions DENIED red through its silence assertion), action-record-predicate-root 9, page-header-predicate-dialect 1, related-toolbar-visible 1, template-gate 0. Every leg printed 'ok restored: blob == HEAD'. Driver hash check: all 5 paths equal their HEAD blob, 'git diff HEAD: empty'. Lock VERDICT command-exit 0. No dist involved: the pin imports src through deep paths and the @object-ui/react alias, and the useActionEngine mutation turning the quick_actions arms red shows src resolution.",
        "Union after the final commit, HEAD b8d647a3c4, clean tree, run through os-verify-lock.sh. (a) pnpm exec vitest run packages/components/ packages/react/ gave 'Test Files 447 passed | 1 skipped (448)', 'Tests 4758 passed | 24 skipped (4782)', lock 'VERDICT command-exit 0'. (b) pnpm exec vitest run packages/plugin-detail/ packages/app-shell/src/providers/__tests__/ packages/core/src/actions/ packages/core/src/evaluator/ packages/permissions/src/__tests__/effectiveObjects-4421.test.tsx plus 98 consumer test files gave 'Test Files 384 passed | 1 skipped (385)', 'Tests 6177 passed | 35 skipped (6212)', VITEST-EXIT=0, lock 'VERDICT command-exit 0'. The consumer set is git grep over *.test.* outside components/react/plugin-detail for page:header, action:group, action:icon, RelatedToolbarButton, RelatedList, quick_actions, RecordQuickActions, useActionEngine and DropdownActionItem, deduped against the listed dirs. The vitest guard's evaluateVitestInvocation accepted that argv before the run. The first attempt was refused by the vitest invocation guard (a positional filter subsumed by a listed dir). That exit 1 is NOT MEASURED, not a red; it was re-run as (b).",
        "Build and type-check at b8d647a3c4 through the lock: turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 gave 'Tasks: 28 successful, 28 total', BUILD-EXIT=0. type-check (tsc --noEmit && tsc -p tsconfig.test.json, script name echoed) exited 0 for @object-ui/components, @object-ui/react, @object-ui/plugin-detail and @object-ui/app-shell. tsc -p tsconfig.test.json --listFilesOnly shows the new pin in app-shell's program, the three edited component tests in components' program, and related-toolbar-visible in plugin-detail's program.",
        "@object-ui/app-shell full suite NOT MEASURED. Reason: its src is untouched (one new test file). The consumer sweep in (b) ran every app-shell test that names a changed surface, and CI runs the rest.",
        "Reach, public door /forms/:name. A throwaway probe, not committed, rendered the real App at /forms/showcase_task.edit with FormPage replaced by an action:icon gated on current_user.can plus a companion. At HEAD it read 'isLoaded=false subjectCarriesPermissions=false current_user.id=u1 gatedIcon=hidden companion=shown'. With the action:icon fix ablated ('ok mutation landed ... blob 13f463e6a9cd -> 4c4a4e983e32', 'ok restored') it read 'gatedIcon=SHOWN'.",
        "Probes for the report, not committed: RowActionMenu item disabled: !current_user.can read 'not-loaded: ENABLED', 'granted: ENABLED', 'denied: DISABLED'. RelatedToolbarButton visible:false read 'SHOWN'."
      ],
      "gates": [
        "node scripts/check-changeset-presence.mjs exit 0: '11 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s): .changeset/11212-permission-gates-fail-closed.md.'",
        "pnpm check:control-bytes exit 0: 'check-control-bytes: OK (scanned 9626 tracked text file(s); skipped 85 binary).'",
        "pnpm check:action-forward-parity exit 0 (action:icon / action:group / action:menu 'payload excess-property CHECKED')",
        "pnpm check:new-line-citations exit 0: 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'",
        "pnpm check:doc-types exit 0: 'Every documented component type is registered.'",
        "pnpm check:doc-fences exit 0 · pnpm check:test-path-roots exit 0 · pnpm check:vi-mock-specifiers exit 0",
        "pnpm check:changeset-claims exit 0 (report-only). It named 7 pending changesets citing touched files; each paragraph was read, and none states a visible/disabled fault policy.",
        "pnpm check:pending-changeset-literals exit 0 · node scripts/check-changeset-no-major.mjs exit 0",
        "Lint, proven narrowing (the CI per-package 'eslint .' form; the full lint run is CI's). pnpm exec eslint --format json over the 11 touched .ts/.tsx files: JSON count 11 files, 0 errors, 241 warnings, exit 0. Base vs head per file, with base read through --stdin --stdin-filename: every file is equal to base except the new pin, which has 2 warnings (@typescript-eslint/no-explicit-any on React.ComponentType of any, the currentUserCan-4421 pin's pattern). Two new no-explicit-any warnings the first commit added were removed in b8d647a3c4. Population: eslint.config.js as configured. Invariance: the config is not type-aware, so the diff cannot move a verdict on an untouched file.",
        "CI at first read of head b8d647a3c4: 42 check-runs, 21 success, 18 in_progress, 3 skipped, 0 failure. Convergence belongs to the PM."
      ],
      "line_budget": "n/a: no skills/** and no governed file (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**) in the diff",
      "files_changed": [
        ".changeset/11212-permission-gates-fail-closed.md",
        "content/docs/layout/page-header.mdx",
        "packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx",
        "packages/components/src/__tests__/page-header-predicate-dialect.test.tsx",
        "packages/components/src/renderers/action/__tests__/action-record-predicate-root.test.tsx",
        "packages/components/src/renderers/action/__tests__/action-template-predicate-gate.test.tsx",
        "packages/components/src/renderers/action/action-group.tsx",
        "packages/components/src/renderers/action/action-icon.tsx",
        "packages/components/src/renderers/layout/containers.tsx",
        "packages/plugin-detail/src/RelatedList.tsx",
        "packages/plugin-detail/src/__tests__/related-toolbar-visible.test.tsx",
        "packages/react/src/hooks/useActionEngine.ts",
        "packages/react/src/hooks/useExpression.ts"
      ],
      "deviations": [
        "Surface vs the claim. The header disabled fix landed in packages/components/src/renderers/layout/containers.tsx, not in listConditional.ts: the direction is the caller's fallback, measured. listConditional.ts and ActionRunner.ts are untouched. Added beyond the listed files, each named in the PR body: the new pin under packages/app-shell (test only); four existing pins flipped or labelled; content/docs/layout/page-header.mdx, whose sentence 'a disabled predicate that faults leaves the action enabled' this change made false; and a comment-only correction in packages/react/src/hooks/useExpression.ts, which listed the throwing legs.",
        "Zone 2 assumption 2: not a bare copy of the option. action:group got a same-file hook, useMemberVisible, shared by its two leaves, as action:menu's useMenuActionVisible is. action:icon and the group host copy the option directly.",
        "Zone 2 assumption 5, half falsified: the dashboard header actions have no visible leg (see summary). The binding is in useActionEngine only; the runner's user / ctx.user / os.user stay the host's object, which carries systemPermissions.",
        "The changeset declares @object-ui/components, @object-ui/plugin-detail and @object-ui/react as minor (Clause-② yes). The presence gate counts 4 released packages, because app-shell's src gained a test file. app-shell is not in the frontmatter because nothing it ships changed.",
        "PR footer: the os-dev session-URL form, not the harness's emoji line. Commit trailers: the AGENTS.md model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness's model-named trailer.",
        "Incident, reported so it is not hidden. While editing the PR body in the scratchpad I used an unquoted heredoc, and bash ran the backtick-quoted command text in /home/user. Every command failed without touching anything. The command names were not found. pnpm answered ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND or RECURSIVE_EXEC_NO_PACKAGE. node scripts/check-changeset-presence.mjs gave MODULE_NOT_FOUND. tsc gave TS5058. A global eslint died importing a parser, with no --fix. No vitest or turbo run started. Afterwards: /home/user holds no new entries, and the worktree is clean. The body was rewritten from a file and read back byte-equal after pr_create (11301 bytes sent, 11301 stored, identical).",
        "Lock: two waiters hit queue-timeout 99 and resumed their kept slots (issue-11212-build, issue-11212-final); both reads above come from the resumed runs.",
        "Throwaway probe files were created under apps/console/src/__tests__, packages/app-shell/src/providers/__tests__ and packages/plugin-detail/src/__tests__ and deleted after each run. None was committed; git status was clean at the union run."
      ],
      "mcp_calls": "0. No MCP GitHub tool was called. Reads went through REST GET: the card and its 3 comments, comments 5321072032, 5911355276 and 5911520748, PR #11237's list, body and check-runs.",
      "api_writes": "3, all through the fleet relay. Each is one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectui/pulls, draft #11237, relay run 36744836119 success. (2) label-write.mjs --assign os-warren: POST /repos/objectstack-ai/objectui/issues/11237/assignees, relay run 36744950374 success, read-back 'MATCHES the target'. (3) post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/11212/comments (this report). git push is not a REST write; the branch was pushed 3 times: the empty branch, 8d44406f79 and b8d647a3c4.",
      "open_questions": [
        {
          "question": "Rider 1 on the remaining disabled legs: fork, not done here. The header's disabled leg now fails closed (DISABLED). The same permission-shaped gate still fails OPEN on the other evalRowPredicate-family disabled legs. Measured: the RowActionMenu item with disabled: !current_user.can('account','delete') reads ENABLED / enabled / disabled (not loaded / granted / denied); its own comment says 'disabled fails soft (not disabled)'. By the same code shape (fallback: false), not measured: the data-table row action's disabled, and the built-in disabledWhen on RecordDetailView / DetailView / ObjectView / ObjectDataPage, whose fail-soft posture PR #4515 documented ('an unevaluable predicate must not grey a button forever'). The not-loaded state is reachable only outside MePermissionsProvider (/forms/:name, embeds). Where does the Rider 1 disabled direction stop?",
          "options": [
            "A: one sub-issue of objectui#4421 flipping the two authored-action disabled legs, RowActionMenu item and data-table row action, to fallback true, as the header now does. Built-in disabledWhen stays untouched. BUSINESS NEED: page-header.mdx tells authors that the row menu evaluates the same predicate as the header, so one declaration now gets DISABLED on the header and ENABLED on the row menu while the payload loads; measured, not speculative. LONG-TERM: one fail direction per key across every authored-action surface, and the useCondition-family legs are already DISABLED on fault. AI ERROR-RESISTANCE: an author or AI writing disabled: !current_user.can gets the same answer on every surface, so no surface-dependent behaviour has to be learned. STARTUP SCOPE: two call sites, one per file, no new surface, no new gate.",
            "B: A plus the built-in disabledWhen. BUSINESS NEED: weak. The built-in Edit/Delete are already permission-gated by the affordance rules, so a permission-shaped disabledWhen on them is redundant. LONG-TERM: uniform, but it reverses a documented, pinned posture (PR #4515) on a different key. AI ERROR-RESISTANCE: marginal. STARTUP SCOPE: more pins to flip and a documented contract to rewrite.",
            "C: stop at the record header, since the card's table named only that leg. BUSINESS NEED: the reach is narrow. LONG-TERM: leaves one key with two fail directions by surface. AI ERROR-RESISTANCE: worst of the three; the same metadata means different things by placement. STARTUP SCOPE: zero work."
          ],
          "recommendation": "A. The inconsistency is measured and reachable through documented authoring (one declaration, header plus row menu). The fix is two mechanical fallbacks that match both the header and the useCondition-family legs. B pays a documented-contract reversal for no measured need."
        }
      ],
      "out_of_scope_findings": [
        "class: b · Seam: spec:current_user.can (ADR-0068 D1 alias set; objectui#4421 Rider 1) → renderer:RowActionMenuItem disabled leg (plugin-grid) and data-table row action disabled leg (components) · reach: NOT a public door. The production RowActionMenu was measured under jsdom with the real ExpressionProvider and MePermissionsProvider: ENABLED / enabled / disabled. · evidence: the probe above, plus RowActionMenu's own comment 'disabled fails soft (not disabled)' · disposition: open question 1, a sub-issue candidate of objectui#4421 (Rider 1 scope). The seat files it. · dedupe: 'row menu disabled current_user.can', 'RowActionMenuItem disabled fail-soft', 'Rider 1 disabled legs'",
        "carrier: none (承接者:无) · noted in PR #11237 Acceptance notes, not filed · RelatedToolbarButton gates visible on truthiness (visiblePred && !isVisible), so a list_toolbar action authored visible: false renders. The probe read SHOWN, and RelatedRecordActionsBridge's deriveActions passes visible through unfiltered. This is the objectui#3812 declared-gate class, a different defect class from this card, so no bounded in-place fix was made. No public-door reading and no named producer, so it is not filed.",
        "carrier: none (承接者:无) · noted in PR #11237 Acceptance notes, not filed · ActionRunner.execute's disabled-gate comment says an eval failure 'defaults to NOT-disabled', but the code blocks. This change rules DISABLED as the direction for disabled, so only the comment is wrong. It is the PR #11208 neighbour, untouched.",
        "carrier: none (承接者:无) · noted in PR #11237 Acceptance notes, not filed · the ActionProvider runner itself still binds no current_user. useActionEngine writes it onto a shared provider runner, the same way it already writes record / recordId / objectName, so a page with a record:quick_actions block also answers current_user in that provider's execute gates, and a page without one does not. This is the PR #11208 neighbour, where a header action with disabled: !current_user.can is refused by ActionRunner.execute for a grant holder. Not re-measured here."
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11212,
      "patch_round": 1,
      "status": "done",
      "branch": "claude/issue-11212-rider1-fail-soft-legs",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11237",
      "session": "session_0122Knsowci76D2rBWReCzzZ",
      "premise_still_valid": true,
      "head": "cb561e78bc46f35cbfd46619600df116f10830ef",
      "summary": "The blocking finding of contract review 5915761337 is addressed. The changeset headline no longer claims 'on every action surface' for disabled. It now reads: an action gated on current_user.can stays hidden until the permissions payload has loaded on every action visible surface, a page:header action gated through disabled stays disabled, and record:quick_actions can answer current_user at all. Nothing else changed: no source, no test, no other prose.",
      "diff": "1 file, 1 line: .changeset/11212-permission-gates-fail-closed.md, the headline line only (1 insertion, 1 deletion). Commit cb561e78bc on top of b8d647a3c4, pushed without force.",
      "tests": [
        "node scripts/check-changeset-presence.mjs exit 0: '11 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s): .changeset/11212-permission-gates-fail-closed.md.'",
        "pnpm check:changeset-claims exit 0 (report-only): self-contradiction section 'Every package declared across those 1 body(ies) is either not negated'. The 7 pending changesets that name touched files are the same list as last round.",
        "Control-byte self-scan of the edited changeset: 0 hits. No test was re-run, because the diff is one prose line in a changeset."
      ],
      "mcp_calls": "0",
      "api_writes": "1: post-stamped.mjs POST /repos/objectstack-ai/objectui/issues/11212/comments (this report), through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches). git push (1, fast-forward b8d647a3c4 to cb561e78bc) is not a REST write.",
      "deviations": [
        "The commit message went through a QUOTED heredoc (git commit -F - with 'EOF'), so no expansion was possible. The changeset line was edited with the Edit tool, and this report was written with the Write tool."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11237 at cb561e78b; ready and auto-merge follow this ACCEPT

    domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T17:10Z. Checked against the diff and the head's check-runs. Claim 5913814153; dev reports 5915507971 and patch round 1 5915791474.

    • Contract review at CONTRACT_REVIEW_TIER: round 1 5915761337 on b8d647a3c, FAIL on one blocking finding (the changeset headline claimed "(or disabled) … on every action surface"); patch round 1 changed that one line; delta record 5915883213 PASS on this head, the landing head. Round 1's ① and ③ carry over: the source, test and doc diff is byte-identical between the heads. Implemented-by: claude/issue-11212-rider1-fail-soft-legs, Reviewed-by: session_0122Knsowci76D2rBWReCzzZ.
    item reading
    Rider 1 (5321072032) measured through the real MePermissionsProvider → ExpressionProvider → ActionProvider and the registered renderers: every visible leg on the card (action:group inline member, dropdown member and host; action:icon; RelatedToolbarButton) now reads hidden / shown / hidden (not loaded / granted / denied), and the record header's disabled: !current_user.can(...) reads DISABLED / enabled / disabled
    one policy per key the visible legs take throwOnError, the branch action:button / action:menu / action:bar already take, so any faulting visible hides there, not only a can() fault (the card's option A; stated in the changeset and page-header.mdx)
    the header disabled the fallback moved in evalHeaderPredicate (containers.tsx); evalRowPredicate returns the caller's fallback, so listConditional.ts is untouched and the row menu, data table and bulk fold do not move
    the added leg (5911534880) useActionEngine binds the predicate-scope subject as current_user, the same object the scope carries the permissions map on, so record:quick_actions answers current_user.can(...) at all; the dev's premise correction holds (the dashboard header defs carry no visible)
    red first, ablation 14 of 28 new-pin arms red on the base, exactly the card's table; five ablation legs turned 25 tests red where predicted, each restored to blob == HEAD
    semver components, plugin-detail and react minor; Clause-②: yes; the headline now names every action visible surface and scopes disabled to page:header
    CI head cb561e78b: 43 runs, 40 success, 3 expected skips (the coverage matrix and dependabot), 0 failure; Type Check and Spec Main Shape Gate success
    main since the merge base no file of this PR changed on main since dded788ada; a test merge onto main is clean
    scope / governed 13 files, +618 / −75; check-governed-merges.mjs: not governed; Fixes #11212

    Deviations, accepted as the records answer them: the header fix in containers.tsx, not the claim's listConditional.ts (measured: the caller owns the direction); a same-file useMemberVisible for action:group's two leaves; four existing pins flipped, the page-header.mdx sentence corrected and a useExpression.ts comment enumeration updated (AGENTS.md #2); @object-ui/app-shell gains only a test file and owes no changeset line. The dev's reported incident (an unquoted heredoc that ran backticked text in /home/user, all commands failing) left no residue: the diff is the 13 listed files and the tree was clean.

    Open question, answered by this seat: A. The row-menu item and the data-table row action keep a fail-OPEN disabled while the payload loads, while the header now fails closed; that is Rider 1 residue on authored actions, so it reuses the ruling. The built-in disabledWhen stays fail-soft (its posture is documented and pinned; option B not taken). → filed objectui#11242 (sub-issue of objectui#4421, pm:queue, p2), which also carries the review's page-shaped current_user on the ActionProvider runner and the ActionRunner.execute comment.

    Out-of-scope findings:

    • RelatedToolbarButton renders a toolbar action authored visible: false (a truthiness gate; the objectui#3812 declared-gate class; reproduced through the real component) → filed objectui#11244 (finding, for triage), as the review asked for a carrier.
    • the ActionRunner.execute disabled-gate comment contradicts its code → carried by objectui#11242.
    • the provider runner answers current_user only on pages that mount record:quick_actions / record:alert / a dashboard → carried by objectui#11242.
    • on the runner bag user.can(...) still faults while current_user.can(...) answers → Acceptance notes (a doc note for objectui#11242's change).

    Parent. objectui#4421 stays pm:blocked; when this card closes its Blocked-by: moves to objectui#11242.

    domain:ui seat 1 · ACCEPT · 2026-09-30T17:10Z


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11237 merged as 8bab1571d9; closed completed

    domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T17:56Z.

    • PR objectui#11237 (Fixes #11212) merged through the merge queue as 8bab1571d9, which is on origin/main. The merge closed the card by its Fixes keyword. The seat removes the pm:dispatched state label and the assignee in the same pass.
    • It merged at 2026-09-30T17:27:39Z. Verified by content: all 13 of its files on origin/main are blob-identical to the landing head cb561e78b.
    • What ships: Rider 1 on the fail-soft legs. While the permissions payload has not loaded, every visible leg on the card hides a permission-gated action. Those legs are the action:group inline and dropdown members and host, action:icon, and RelatedToolbarButton. The record header's disabled reads disabled. record:quick_actions answers current_user.can(...) through the same subject. The record is 5915761337 (round 1) plus the delta 5915883213 (at-tier PASS on the landing head). The ACCEPT is 5916059211.
    • What stays, and who holds it:
      • The row-menu and data-table row-action disabled legs, and the ActionProvider runner answering current_user only on some pages: objectui#11242 (pm:queue, priority:p2). The seat decided it in-seat at the ACCEPT, as option A.
      • RelatedToolbarButton renders an action authored visible: false: objectui#11244 (finding, for triage).
      • objectui#4421's Blocked-by: moves from this card to objectui#11242 in the same pass.

    domain:ui seat 1 · landed · 2026-09-30T17:56Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions