Skip to content

Console: send a same-origin type: 'api' action click through POST /actions/:object/:action instead of fetching its target directly (ruling on #27506, step 3 of 3) #27611

Closed

Description

@objectstack-fleet

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

立卡门:③ 维护者直派(ruling on #27506, comment 6499781492)。

What this card is

Step 3 of the three-step ruling on #27506 (comment 6499781492). The maintainer's words, verbatim and untranslated (card thread of #27506, 2026-10-29): 「我们的平台设计目标是 agent 和人类使用同一套系统」 · 「同意」.

Blocked-by: #27506 (step 2, the server door accepting type: 'api' declared actions and forwarding to the declared target). Do not start before that PR is on main.

Business reason

Today the console's two api handlers fetch an api action's target directly (ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx apiHandler, ui/packages/app-shell/src/views/RecordDetailView.tsx apiHandler), so the click never meets the platform's gates: switching Ban User off in Setup › Packaged automation does not stop the button (#27506), and the human click and an agent's run_action take different roads. After step 2 the door runs the ADR-0126 switch, ADR-0066 D4, the anonymous baseline and the audit, then forwards to the same target; the console only has to send the click there.

Scope

  • Both api handlers: for a declared type: 'api' action whose target is a same-origin platform path, POST /api/v1/actions/:object/:action (or /actions/global/:action) with { recordId, params } the way the script handler (serverActionHandler) already does; the server assembles the target body from the declaration (recordIdParam, bodyShape.wrap, bodyExtra, method, target placeholders). Page-variable tokens ({{page.x}}) are client state: keep resolving them client-side and send the resolved values as params.
  • An api action whose target is not same-origin keeps today's direct fetch (the server refuses to govern it; see the ruling).
  • Keep the newTabUrl fast path (withNewTabUrlFastPath), the entitlement-dialog mapping of coded 403s, the { success, data } unwrapping and refreshAfter as they are; the response shape from the door is the forwarded target's data.
  • Pins: a switched-off api action's click answers the door's 409 ACTION_DISABLED and nothing is sent to the target; a switched-on click reaches the target through the door with the same body as before (compare the recorded request); an off-origin target still goes direct.

Who acts

ui lane. Sibling #27504 (the switched-off button stays visible and its toast wording) is a separate card and stays open.

Dedupe

Searched issues for "Ban User button stays live after packaged action switched off" (5 hits: #27506, #27504, none on the console sending api clicks through the door) and "api type action dispatched through platform action route" (11 hits, none). Dedupe words: apiHandler, type api, /actions door, useConsoleActionRuntime, RecordDetailView


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 审批与自动化 | none | P②

    Triage: p1 · domain:ui · area:automation · pm:blocked on #27506 · 2026-10-29T15:10Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:skills#1 · marchtian · session_017ihL2dPRZXfgE1A2Vct3FU

    For step 3 of the #27506 ruling · 2026-10-29T18:33Z · ⛔ not a claim.

    PR #27664 (card #27610) amends ADR-0066 D4 and the requiredPermissions .describe() to the ruling. These ui sites still quote the superseded text ("action route (script/flow/modal + MCP) …" / "never for a type: api action pointed at a custom endpoint") and fall to this card:

    • ui/packages/components/src/renderers/layout/containers.tsx ~L2015
    • ui/packages/components/src/__tests__/action-bar.test.tsx ~L408
    • ui/packages/components/src/__tests__/page-header-capability-gate.test.tsx ~L12
    • ui/packages/plugin-grid/src/__tests__/bulkActionCapabilityGate.test.tsx ~L15
    • ui/packages/plugin-detail/src/RelatedList.tsx L810
    • ui/packages/plugin-detail/src/__tests__/RelatedList.toolbarCapabilityGate-9782.test.tsx L14

    The last two were found by the at-tier contract review 6505129020 on #27664.

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    Note for whoever claims this card (from the ADR contract review on #27664, record 6505129020): the file list above misses one console surface that restates the old ADR-0066 D4 api clause. ui/packages/plugin-detail/src/RelatedList.tsx (around L810, the list_toolbar docblock quoting "enforced with 403 on the platform action route (script/flow/modal + MCP)") and its pin ui/packages/plugin-detail/src/__tests__/RelatedList.toolbarCapabilityGate-9782.test.tsx (around L14). Once #27610's ADR text and #27649 land, an api click through the door is enforced at the route too, so read both against the new clause when this card is cut.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    Claim: PM loop round 1
    Session: session_01Utp6CbDAucNyQ4UtzCJKyh
    Account: os-bill
    Branch: claude/project-thread-d3yu0i (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27611
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (apiHandler and direct helpers), ui/packages/app-shell/src/views/RecordDetailView.tsx (apiHandler only; seat 2's #27640 edits fmtCtx in the same file and spec draft PR #27713 also touches it: different regions, second to merge resolves), ui/packages/app-shell/src/utils/flowResponse.ts if the door envelopes need it, the six ADR-0066 D4 docblock/pin sites of comments 6505154678 and 6505165215 (components containers.tsx + two tests, plugin-grid bulkActionCapabilityGate test, plugin-detail RelatedList.tsx + its 9782 test), tests beside, root .changeset/ (stop on breach; explain in the report)
    Container & model: L, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier on ecd944f5: no path-derived mandate), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: same-origin type: api clicks POST through /api/v1/actions/:object/:action; off-origin stays direct; pins per the card (409 ACTION_DISABLED sends nothing to the target; switched-on body unchanged; off-origin direct)
    Thread-read: 6505165215
    Premise: holds
    Serial constraints cleared: step 2 #27506 (PR #27649) and ADR amendment #27610 (PR #27664) merged; RecordDetailView overlap with #27640 (seat 2) and PR #27713 (spec) declared

    Dispatched 2026-10-29T21:46Z to a card thread of the seat's project; the order lives in the project. ⛔ The thread posts no claim and writes no assignee.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CXnX1eGkqKFsNRxR8H

    os-dev-report

    {
      "issue": 27611,
      "status": "done",
      "branch": "claude/project-thread-d3yu0i",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27793",
      "session": "session_01Utp6CXnX1eGkqKFsNRxR8H",
      "premise_still_valid": true,
      "summary": "Both console api handlers send a click on a DECLARED same-origin type api action to POST /api/v1/actions/:object/:action with { recordId: subject row id, params } through core's createServerActionHandler; off-origin targets, clicks naming no declared action and object-less declared actions keep the direct fetch. The door is chosen by lookup in the host's object metadata (new module-internal utils/apiActionDoor.ts: isSameOriginApiTarget restating the server predicate, findDeclaredApiAction tolerant of runner-interpolated ${...} tokens, sendApiActionThroughDoor, readApiActionDoorSuccess). Non-2xx answers keep each host's reader (entitlement dialog then apiActionFailureMessage in the list runtime, apiActionFailureMessage on the record page), so 409 ACTION_DISABLED shows its sentence; 2xx reads the door's data through the /actions envelope rule, so a bare better-auth payload behind a resultDialog reads at the same level as before. Six docblock/pin sites aligned to amended ADR-0066 D4. Server gaps G1 (door drops error.details/userMessage) and G2 (param contract refuses approval outputs.<key>) were stopped on and reported; the seat filed #27738 and ruled Route A, so the PR is draft with Blocked-by: #27738 and the three regression pins are written against the door's post-#27738 answer, shown only against a mocked door. Opened with no assignee and no review request (maintainer rule); the harness after-PR assign/review step was skipped on purpose.",
      "tests": "Final head b6ee6801 (post-merge), lock VERDICT command-exit 0: vitest on 3 new + 7 updated files, Test Files 10 passed, Tests 71 passed; pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit + tsconfig.test.json) exit 0. Affected surface on bd2818b4 (714 ui test files selected by grep for api actions, api handlers, the /actions route, useConsoleActionRuntime, RecordDetailView, ObjectView, PageView): Test Files 714 passed, Tests 8944 passed, VERDICT command-exit 0. Ablation via scripts/ablation-replace.mjs on source-imported suites (anchor 1 -> 0, blob 82d30ed8bd08 -> b3f537e95b1e), marker 'ABLATED_27611' making findDeclaredApiAction answer undefined: 10-file set 27 red / 44 green (every updated pin red); after strengthening two pins that read alike on both roads, the 3 new files 17 red / 8 green (every door-road case red; the 4 direct-road controls, the predicate and negative lookup cases green); restore blob == HEAD 82d30ed8bd08, git diff HEAD empty.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on b6ee6801: 34 derived, 34 run, all exit 0 (incl. check-sdui-manifest.mjs and --self-test, check:nul-bytes), lock VERDICT command-exit 0; --ran reconciliation: 34 run, 0 NOT-MEASURED, 0 UNRUN. Outside derivation: pnpm --dir ui run check:new-line-citations exit 0 (0 new citations); app-shell type-check exit 0. CI on b6ee6801 at report time: Governed Surface Queue Guard success; Check Changeset, Check PR Size, Lint (8 lanes), Type Check (4 lanes), PR-body guards, filter in_progress; three opt-in lanes skipped.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool used. Non-GitHub MCP: mcp__claude-code-remote__get_session x1 (session id), mcp__claude-code-remote__subscribe_pr_activity x1 on PR 27793 (this session's event subscription; writes nothing to GitHub).",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 40973109951, PR 27793, body read back identical, assignees none); this report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/27611/comments). git push x3 not counted (not REST).",
      "line_budget": "20 files, +1044 / -65 vs origin/main (git diff --shortstat), under the 3000 human-merge threshold",
      "files_changed": [
        ".changeset/27611-api-action-click-through-door.md",
        "ui/apps/console/src/services/approvalRequestsDataSource.decisionPage.test.tsx",
        "ui/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.apiActionDoor-27611.test.tsx",
        "ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
        "ui/packages/app-shell/src/utils/__tests__/apiActionDoor.test.ts",
        "ui/packages/app-shell/src/utils/apiActionDoor.ts",
        "ui/packages/app-shell/src/views/ObjectView.inviteFeedback-11817.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.accessEndedAfterDecision-26589.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.apiActionDoor-27611.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.apiNewTabUrl-26747.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.approvalDeclaredActions.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.printPageHeaderActions-12906.test.tsx",
        "ui/packages/app-shell/src/views/RecordDetailView.tsx",
        "ui/packages/app-shell/src/views/__tests__/PageView.relatedListActions-12870.test.tsx",
        "ui/packages/components/src/__tests__/action-bar.test.tsx",
        "ui/packages/components/src/__tests__/page-header-capability-gate.test.tsx",
        "ui/packages/components/src/renderers/layout/containers.tsx",
        "ui/packages/plugin-detail/src/RelatedList.tsx",
        "ui/packages/plugin-detail/src/__tests__/RelatedList.toolbarCapabilityGate-9782.test.tsx",
        "ui/packages/plugin-grid/src/__tests__/bulkActionCapabilityGate.test.tsx"
      ],
      "deviations": [
        "RecordDetailView.tsx (hot file, seat ruling): the edit stays inside apiHandler plus its import line and `objects` added to that handler's useCallback dependency list; origin/main merged before the PR opened.",
        "Record page 2xx level: the record page's direct fetch handed back the target's whole body; on the door road it hands back the door's data (the list runtime's level). Without this a bare better-auth answer behind a one-shot resultDialog (rotate_client_secret, enable_two_factor on record pages) would come back wrapped and the dialog would read nothing. A relayed HTTP 200 rejection is now a failure on the record page too. Declared in the PR acceptance notes.",
        "The 714-file affected-surface run is a grep-selected subset, not the full ui suite, and ran on bd2818b4 (before the pin-strengthening commit and the origin/main merge); the final head was re-measured on the 10 card files plus app-shell type-check. CI runs the full farm.",
        "Harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; followed os-dev instead (model-free trailer pair on every commit incl. the merge commit, session-URL PR footer).",
        "PR body attribution line links the seat thread exactly as the dispatch order spells it (thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: #27738 · server door drops the target's error.details and userMessage, and its param contract refuses approval decision outputs.<key> params on approval_approve / approval_reject; filed by the seat",
        "carrier: seat (suggested for #27738) · no server test pins buildApiActionRequest's outputs.* fold, which the console now relies on",
        "carrier: none · noted, not filed — by code reading only (not reproduced): a target answering HTTP 200 { success: false, data } loses its rejection at the door (apiActionResultOf returns data), and 200 { success: false, message } with non-legacy keys reads as success under the narrow /actions rule; no producer measured; dedupe words: apiActionResultOf, 200 success false, action door forward",
        "carrier: none · noted, not filed — object-less declared api actions (/actions/global/:action) and standalone action rows are invisible to the console lookup, so their click stays direct and their activation switch does not hold on a click; dedupe words: global api action, action door, lookup"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    ACCEPT → PR #27793 comment 6519049786 · 2026-10-30T09:03Z


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions