Repository navigation
fix(app-shell): a declared flow action's button starts its flow through the action door (objectui#12037) - #12046
Merged
Conversation
…gh the action door (objectui#12037) Both console flow-launch handlers started every flow through POST /api/v1/automation/:flow/trigger, so a declared flow action's own server-side gates (requiredPermissions per ADR-0066 D4, activation, param contract, caller-scoped subject load) never ran for the click. Triage ruling B: a click whose object and name resolve to a declared type 'flow' action with the same target, in the object metadata the console already holds, goes through POST /api/v1/actions/:object/:action (request built by core's createServerActionHandler, answer read by the flow rule). Every other flow start (an inline page button with no or a non-matching name, a dashboard header action) stays on the trigger route: it has no action gate to apply, and ruling A's elevation refusal holds there. One module-internal launcher, utils/flowLaunch, serves both handlers. The record page's related-list toolbar flow action (no row) no longer sends the parent page's id against the child object. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
… starts through the action door (objectui#12037) The live lane's `screen-flow.spec.ts` asserted a POST to /api/v1/automation/:flow/trigger. Under triage ruling B the declared `showcase_bulk_reassign` row action now starts its run through POST /api/v1/actions/showcase_task/showcase_bulk_reassign, so the run paused, rendered its screen, resumed and wrote the row, and only that request-shape assertion failed. The spec now asserts the action-door launch and that no trigger-route start happened; the resume and persisted-data assertions are unchanged. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
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.
Fixes #12037
Clause-②: no
Built on triage ruling B (card comment
6077692839): a flow click goes through the action door exactly when it names a declared flow action; every other flow start stays on the trigger route. Ruling A landed in objectstack-ai/objectstack#22424. Implemented by sessionhttps://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU(seatdomain:ui#2).What changes
Both console flow-launch handlers used to start every flow through
POST /api/v1/automation/:flow/trigger:flowHandlerinuseConsoleActionRuntime(list, page and root runtime) andflowHandlerinRecordDetailView. That route starts a flow by name, so a declared flow action's own server gates never ran for the click. Those gates are itsrequiredPermissions(ADR-0066 D4, whose console half only hides the button), its activation switch, its param contract and the caller-scoped load of its subject record.launchConsoleFlowin the newpackages/app-shell/src/utils/flowLaunch.ts, which both handlers call. A click goes throughPOST /api/v1/actions/:object/:actionexactly whenfindDeclaredFlowActionfinds, in the object metadata the console already holds, an action of the click'snameon the click's object withtype: 'flow'and the sametarget. Otherwise the click stays on the trigger route. Both handlers' docblocks record why: an undeclared flow start has no action gate to apply, and ruling A's elevation refusal already holds on the trigger route. Those starts are an inline page button with no or a non-matchingname, and a dashboard header's synthesizedname: actionUrl./actionsrequest is built by core'screateServerActionHandler(name-only identity,{ recordId, params },_rowRecordstripped), so app-shell still names that route nowhere (actions-envelope.ratchet.test.tsstays green, unedited). The answer is the flow'sAutomationResult, read byinterpretFlowResponse/judgeFlowLaunchthrough an injected transport. So a paused screen still opensFlowRunner, a refused end still opens the Close-only notice, andFLOW_FAILEDstill prefers the author'serrorMessage. Core's owninterpretActionResponsehas none of those arms.RecordDetailView's flow path no longer hands the parent page's record id to a child object's action that carries no row. A related list's toolbar flow action is now an object-level start; a row action still addresses the child row.content/docs/guide/architecture-overview.md, the Supported Action Typesflowrow now says what the type does and which endpoint each kind of click uses.flow-envelope.ratchet.test.tsnames the callers that are true now:utils/flowLaunch.ts(launch) andviews/FlowRunner.tsx(resume). It also pins that both handlers delegate tolaunchConsoleFlow.Nothing published changes: no export, prop or
packages/i18nkey is added.flowLaunch.tsis reachable only through the two handlers. No spec change, and nameless flow buttons are not refused (ruling B, not A).Measured before building
The door was read at objectstack origin/main
081e6a09, which containsc572fe2a36(git merge-base --is-ancestorexit 0); the files read weredispatchFlowActionand the flow arm ofhandleActionsRequest.deps.success(result)/deps.error(...), the same builders as the trigger route (ADR-0112)runId,screen,flowLabelAutomationResultasdata; a paused result is neither a refusal row nor caught by the residualsuccess === falsearmstatus: 'refused',refusalMessagedeps.successunchangedsuccessMessage400witherror.codeFLOW_FAILEDanderror.details.errorMessage, viaFlowActionRefusalThe door does not carry the flow name.
FlowRunnerresumes under the handler's flow, which on the door path is the declaration'starget; the lookup requires the two to be equal.Related-list row action on a child.
RelatedRecordActionsBridge.runRowActiondispatches the child object's declared action withobjectNameset to the child andrecordIdto the child row. At the door that isPOST /api/v1/actions/CHILD/CHILD_ACTION_NAME.resolveRouteActionDeclarationresolves the declaration on the child.loadActionSubjectRecordloads the child row under the caller's scope, andrefuseDeniedSubjectLoadanswers 404 when that row cannot be read.Where the lookup's metadata comes from. It reads object metadata only. The runtime reads the caller's
objectsfirst, then the console store'sobjecttype, awaitingensureType('object')before deciding, as the param dialog already does. The record page reads itsobjectsprop. That metadata carries an object's ownactions[], which includes everydefineAction({ objectName }):defineStack's bound-action merge puts them there. The console's list row actions render fromobjectDef.actionsalone;e2e/live/screen-flow.spec.tsclicks the boundshowcase_bulk_reassignthat way. The metadata does not carry an object-less declared action, nor a standalone authored action row that no object definition embeds. A click on either keeps the trigger route, as every flow click did before, so that is no regression; a pin holds it as a control. No click-timeactionmetadata fetch was added.Pins
useConsoleActionRuntime.flowActionDoor-12037.test.tsxruns through the realConsoleActionRuntimeProvider, runner andFlowRunner:requiredPermissionsthe caller lacks is refused by the server with403PERMISSION_DENIED, and its message is the error toast;/api/v1/automation/showcase_reassign_wizard/runs/run-1/resume, the declaration's target;ensureType('object')) when the caller holds no objects;RecordDetailView.flowActionDoor-12037.test.tsxruns through the real record page andFlowRunner:/api/v1/actions/lead_task/escalate_taskwithrecordIdset to the child row, is refused403, and the refusal is shown;Reverse leg. The implementation was committed first (
66291b17c). The trap-guarded script then checked out main's blobs (47b1f0bb7) of the two handler files and ran the two pin files plus the ratchet:Tests 8 failed | 8 passed (16). The 8 failures were every door pin, the two parent-id pins and the delegation pin. The 8 that passed are the trigger-route controls, which hold on main too. After the restore,git diff HEADshowed 0 lines, andgit hash-objectequalled the HEAD blob for both files. Re-run on head:Tests 16 passed (16).Gates, on head
66291b17cflowResponse, the app-shell ratchets,column-identity.ratchet.test.tsandone-authority-per-exported-name-6273.test.ts: 84 files. Run with repo-rootpnpm exec vitest run, under the verify lock, after the final commit:Test Files 84 passed (84),Tests 596 passed (596).pnpm --filter @object-ui/app-shell type-check, echoedtsc --noEmit && tsc -p tsconfig.test.json, gave VERDICT command-exit 0. The upstream closure was built first.--listFilesOnlyon the test tsconfig lists all four new or edited files.check-changeset-presenceandcheck-changeset-no-majorpass.check:new-line-citationsreports0 new citation(s).check:control-bytes,docs:check-links,check:spec-symbols,check-vi-mock-override-shape,check-test-path-rootsandcheck-lint-coverageare all OK.pnpm exec eslinton the 6 touched TS files (count from--format json) gives 0 errors. Warnings on the two handler files equal their base blobs: 110 and 38. The repo'seslint.config.jshas noprojectService/parserOptions.project, so linting is not type-aware and the diff cannot move an untouched file's verdict. The fullpnpm lintandpnpm testare left to CI.First-load bytes
Both handlers ship eagerly: the runtime's code is in the eager
DeclaredActionsBarchunk, and the record page's is in the eagerRecordDetailViewchunk. A consolevite buildunder the lock, fromapps/console/dist/eager-closure.json, giveseagerGzipBytes3,238,611 at base47b1f0bb7and 3,238,879 at head: +268 B gzip, with 289 eager chunks in both builds.Acceptance notes, not changed here
resolveRecordIdoverride). It is outside the flow path this PR rewrites, so it is reported only._rowRecord.idagainst_rowRecord[recordIdField]was not aligned. The door treats the body'srecordIdas the subject row's primary id and seedsrecordIdParamfromrecord[recordIdField]itself (seedFlowActionParams; the spec describesrecordIdFieldas "Row field whose value seeds recordIdParam"). So_rowRecord.idis the read the door wants. Core'sresolveServerActionRecordIdreads_rowRecord[recordIdField]as the record address; the only producer with a non-idrecordIdField(sys_session'srevoke_session) is anapiaction that never reaches that reader.actionmetadata, so seeing it would take a new provenance signal or a click-time fetch; neither was added.packages/app-shellREADME has no section on the flow launch, so nothing there became false.Surface
The two handler files, the two new pin files,
flow-envelope.ratchet.test.ts, the docs page and the changeset. One file outside the claim's literal list: the new module-internalutils/flowLaunch.ts, the shared call shape (H2) the build round asked to keep module-internal.ObjectView.tsxand everything in objectstack are untouched.Generated by Claude Code