Repository navigation
fix(console): a declared same-origin type: 'api' click goes through the action door - #27793
Merged
Merged
Conversation
… the action door Both api handlers (the shared console runtime and the record page) look the click up in the object metadata they hold. A declared `type: 'api'` action whose target is a path on this origin is sent to the action door with the subject row's id and the params bag, so its gates (the sign-in floor, requiredPermissions, the packaged-action switch, the param contract, the subject load) hold for a human click as they do for an agent. An off-origin target and an undeclared click keep the direct fetch. Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H Co-authored-by: Claude <noreply@anthropic.com>
…ocks
New pins for objectstack#27611: the door lookup and same-origin predicate,
the list runtime and record page handlers sending a declared same-origin
api click to POST /api/v1/actions/:object/:action with { recordId, params },
the 409 ACTION_DISABLED sentence, off-origin and undeclared clicks staying
direct, and three regressions written against what the door answers once
objectstack#27738 lands (flow inner refusal, entitlement dialog, decision
outputs as params).
Existing pins that asserted the direct target URL for a declared action now
assert the door route and body. Six docblock sites read ADR-0066 D4 as
amended. Changeset: @object-ui/app-shell patch.
Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H
Co-authored-by: Claude <noreply@anthropic.com>
… level Both api handlers now read a 2xx from the action door through one helper that applies the /actions envelope rule. The record page used to hand back the door's body whole, so a bare target answer (a stock better-auth body behind a one-shot resultDialog reveal) came back wrapped and the dialog's field paths read nothing; it now hands back the target's payload, as the list runtime does, and a relayed HTTP 200 rejection is a failure there too. Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H Co-authored-by: Claude <noreply@anthropic.com>
The switched-on payload pin and the exhausted-pool pin read the same answer on the direct road, so an ablated lookup left them green. Both now assert the request went to the action door. Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H Co-authored-by: Claude <noreply@anthropic.com>
…-d3yu0i Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 29, 2026
Merged
feat(fields): every record reference shows a hover card with the target's declared highlights
#27912
Merged
This was referenced Oct 30, 2026
…-d3yu0i Claude-Session: https://claude.ai/code/session_01Utp6CXnX1eGkqKFsNRxR8H Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh 席位核对ACCEPT · head
Generated by Claude Code |
This was referenced Oct 30, 2026
Merged
8 of 9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Bill · project thread
维护者速读
type: 'api'动作」(列表行、工具栏、页面按钮、记录页抬头和相关列表)时,不再由浏览器直接请求动作的 target,而是发到平台动作门POST /api/v1/actions/:object/:action,只带记录 id 和参数,由服务端按声明组装请求再转发。目标在其他域名的 api 动作、没有对应声明的页面内联按钮、不挂在对象上的全局动作,仍然直连。outputs.<key>参数。在 action door: forwarding a type:api action drops the target's error.details and userMessage, and the ADR-0104 param check refuses approval decision outputs (outputs.KEY), so console step 3 (#27611) would regress #27738 合入之前合本 PR,会让「带决策输出的审批」被拒、套餐升级弹窗缺链接、流程端点的拒绝提示退回节点链。回滚即还原本 PR。ui/packages/app-shell/src/utils/apiActionDoor.ts(选门规则、发送与 2xx 读取);hooks/useConsoleActionRuntime.tsx的 apiHandler 动作门分支;views/RecordDetailView.tsx的 apiHandler 动作门分支。pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/apiActionDoor.test.ts packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.apiActionDoor-27611.test.tsx packages/app-shell/src/views/RecordDetailView.apiActionDoor-27611.test.tsx,预期 3 个文件 25 条全绿。Fixes #27611
Blocked-by: #27738
Clause-②: no
Metadata-protocol: none
What changes
Step 3 of the ruling on #27506: a person's click and an agent's
run_actiontake the same road. The server half is onmain(#27649: the action door accepts a declaredtype: 'api'action whosetargetis a path on this origin, runs its gates and forwards to the target in-process). This PR moves the console's two api handlers onto it.ui/packages/app-shell/src/utils/apiActionDoor.ts(module-internal, not re-exported):isSameOriginApiTargetrestates the server's predicate (action-execution.ts#isSameOriginApiTarget): a path that starts with one/, not//or/\.findDeclaredApiAction(objects, objectName, actionName, clickedTarget)decides the door by lookup, asflowLaunchlooks up a flow action: the click's object andnamemust resolve to a declaredtype: 'api'action whose declaredtargetis same-origin and is the clicked target. A${…}token the runner already interpolated into the target matches one path segment.sendApiActionThroughDoorsends through@object-ui/core'screateServerActionHandler, so the/actionsrequest (name-only identity,{ recordId, params }, the_rowRecordstrip) stays core's and app-shell still names that route nowhere (actions-envelope.ratchet.test.ts). It keeps the raw answer for the host to read.readApiActionDoorSuccessreads a 2xx through the/actionsenvelope rule (interpretActionResponse). The door answers the target's payload as itsdata, already unwrapped from the target's{ success, data }, so the payload is what the direct fetch handed back for a target that answers bare (a stock better-auth body), and a target's HTTP 200 rejection relayed insidedatareads as a failure.useConsoleActionRuntimeapiHandler(list rows, toolbar, page actions): for a same-origin target with an object and a name it waits for the object type (as the flow launch does, so "no declaration" is an answer and not a race against a cold store), looks the action up, and on a hit sends{ recordId: <stashed row's id>, params: <params, {{page.<var>}} resolved> }to the door. A non-2xx goes through the same reader as the direct fetch (refused: the entitlement dialog, thenapiActionFailureMessage). A 2xx returns the payload and honoursrefreshAfter.RecordDetailViewapiHandler(record header, related lists): the same, looked up in the page'sobjects. The subject id is the stashed row's id, else a related-list row's explicitrecordId, else this page's id when the action is on this page's object. A non-2xx readsapiActionFailureMessageas the direct fetch does, andrefreshAfter === truekeeps this page's meaning. The edit stays insideapiHandler, plus its import line andobjectsin its dependency list.action:button), and for a declared action the lookup cannot see (below).withNewTabUrlFastPathstill wraps both handlers, so anewTabUrlclick sends nothing on either road.requiredPermissions; an off-origin target never reaches it):containers.tsx,action-bar.test.tsx,page-header-capability-gate.test.tsx,bulkActionCapabilityGate.test.tsx,plugin-detail/src/RelatedList.tsx,RelatedList.toolbarCapabilityGate-9782.test.tsx.@object-ui/app-shellpatch.Tests
New:
utils/__tests__/apiActionDoor.test.ts(6): the predicate on every spelling the server's predicate answers; the lookup by object, name, type and target, an interpolated${ctx.recordId}target, and no hit for an off-origin declaration, another type, another target, or a missing object, name or metadata.hooks/__tests__/useConsoleActionRuntime.apiActionDoor-27611.test.tsx(12), on the hook's realapiHandler:POST /api/v1/actions/sys_user/ban_userwith{ recordId, params }and nothing reaches the target;409 ACTION_DISABLEDanswers its sentence, never the bare code; a 2xx answers the unwrapped payload and refreshes; a relayed 200 rejection is a failure and does not refresh;{{page.<var>}}tokens arrive resolved inparams.FLOW_FAILEDreads its inner refusal (the form's permission line);DEV_ENV_PLAN_LOCKEDopens the upgrade dialog with its call to action and no error toast;DEV_ENV_LIMITquotes the usage; an approval decision sendsoutputs.<key>as params.views/RecordDetailView.apiActionDoor-27611.test.tsx(7), on the handler set the record page hands itsActionProvider: a header click sends this page's id and hands back the target's payload; a bare target answer comes back at the level the direct fetch handed back, so aresultDialogpath (client_secret) still reads; a relayed 200 rejection is a failure;409 ACTION_DISABLEDanswers its sentence; a related-list child row sends the child row's id to the child object's door; an off-origin target goes direct; theFLOW_FAILEDinner refusal once action door: forwarding a type:api action drops the target's error.details and userMessage, and the ADR-0104 param check refuses approval decision outputs (outputs.KEY), so console step 3 (#27611) would regress #27738 lands.Updated, because they pinned the direct target URL of a DECLARED same-origin action, which now goes through the door (each now pins the door route and its body):
RecordDetailView.approvalDeclaredActions.test.tsx(the decision goes to/actions/sys_approval_request/approval_send_backwith the request's id; the output rides asoutputs.next_reviewerinparams),RecordDetailView.accessEndedAfterDecision-26589.test.tsx,RecordDetailView.apiNewTabUrl-26747.test.tsx(CONTROL),RecordDetailView.printPageHeaderActions-12906.test.tsx(c),PageView.relatedListActions-12870.test.tsx,ObjectView.inviteFeedback-11817.test.tsx, andapps/console/.../approvalRequestsDataSource.decisionPage.test.tsx.Acceptance notes
error.detailsanduserMessage(a flow-backed endpoint's refusal shows the node chain again; an entitlement 403 loses its upgrade/contact links, plan and usage), and the door's param contract refuses an approval decision'soutputs.<key>params (approval_approve/approval_rejectdeclarecommentandattachments), so a decision with declared outputs is refused 400. The four pins in the "once action door: forwarding a type:api action drops the target's error.details and userMessage, and the ADR-0104 param check refuses approval decision outputs (outputs.KEY), so console step 3 (#27611) would regress #27738 lands" group are written against the door's answer after that change and are shown here only against a mocked door.outputs.*fold moved to the server. The console now sendsoutputs.<key>as params and the door'sbuildApiActionRequestfolds them intooutputs. No server test pins that fold today; action door: forwarding a type:api action drops the target's error.details and userMessage, and the ADR-0104 param check refuses approval decision outputs (outputs.KEY), so console step 3 (#27611) would regress #27738 is the place to add one.recordIdParam/recordIdField,bodyShape.wrap,bodyExtra,method, the target's placeholders,organizationId) is pinned server-side inpackages/runtime/src/domains/api-action-forward.test.ts./actions/global/:action) and a standalone authored action row no object embeds are absent from it, so their click stays direct and their switch does not hold on a click. Noted, not filed.organizationIdfor an/api/v1/auth/target now comes from the server's execution context rather than the browser's active organization.{ success: false, data: … }loses its rejection at the door (apiActionResultOfhands backdata), and a 200{ success: false, message }with keys outside the legacy envelope reads as success under the narrow/actionsrule. The direct fetch caught both throughsuccess === false. No producer of either shape was measured.{{page.<var>}}token with no page-variable snapshot is sent as-is; the door refuses it 400, where the direct fetch sent it to the target literally.{ success, data }(the list runtime's level), where the direct fetch handed back the envelope; a bare answer is unchanged, and a relayed 200 rejection is now a failure there too.content/docs/ui/actions.mdxalready describes the door for declared same-origin api actions; no docs change.Verification
Local runs (head b6ee680)
b6ee6801(after mergingorigin/main6d9419b), underscripts/pm/os-verify-lock.sh: vitest on the 3 new files and the 7 updated ones, Test Files 10 passed, Tests 71 passed; thenpnpm --filter @object-ui/app-shell run type-check(bothtsc --noEmitandtsconfig.test.json), VERDICT command-exit 0.bd2818b4(every ui test file naming an api action, an api handler, the/actionsroute,useConsoleActionRuntime,RecordDetailView,ObjectVieworPageView, across app-shell, core, components, the plugins and the console app): Test Files 714 passed, Tests 8944 passed, VERDICT command-exit 0.scripts/ablation-replace.mjs, marker'ABLATED_27611'makingfindDeclaredApiActionanswer no declaration, so every click takes the direct fetch): on the 3 new files, 17 red (every door-road case in both hosts and the two positive lookup cases), 8 green (the four direct-road controls and the predicate and negative lookup cases); on the 10-file set, 27 red including every updated pin. Restore: blob == HEAD,git diff HEADempty. Two door pins that stayed green on the first ablation (they read the same answer on both roads) now also assert the door road.dispatch-gates --commands) onb6ee6801: 34 derived, 34 run, all exit 0 (includingcheck-sdui-manifest.mjsand its--self-test, andcheck:nul-bytes);--ranreconciliation: 0 NOT-MEASURED, 0 UNRUN.pnpm --dir ui run check:new-line-citations: 0 new citations.Generated by Claude Code