Observed while implementing #14143 on 66ecc50a. ⛔ Unclaimed. Filed rather than fixed: #14143's dispatch order binds that card to the handler predicate, and this is one surface over.
What #14143 fixed, and where it stops
#14143 adds ctx.recordLoadDenied to the script/body action face: when the dispatcher's caller-scope load of the subject row does not deliver it, the handler is told, instead of only receiving ctx.record = { id: recordId } (the id being stamped on precisely because the load failed).
Both dispatch doors now route their load through one producer, loadActionSubjectRecord (packages/runtime/src/action-execution.ts). But the flow branch consumes the same record object and carries no equivalent signal:
packages/runtime/src/action-execution.ts — MCP run_action, the action.type === 'flow' branch passes record into dispatchFlowAction;packages/runtime/src/domains/actions.ts — REST /actions, same;dispatchFlowAction then hands it to automation.execute(action.target, { record, ... }) as the run's AutomationContext.record, and seeds params from the same object via seedFlowActionParams.
So a flow started by a row-scoped action on a row its invoker cannot read receives record = { id: <recordId> } — shaped exactly like a legitimate record-less start, with nothing on the context distinguishing the two.
Why it is a smaller claim than #14143, not the same one
A flow action is deliberately not system-elevated: the flow engine receives the caller's identity and honours runAs (ADR-0049), so a runAs: 'user' flow re-derives the caller's scope on its own reads and the stub simply resolves to nothing. The exposure is narrower and conditional:
⛔ Not claimed
Dedupe
One targeted search_issues (repo-scoped REST search answered 403 for this seat, so the channel switch is declared here). 11 results, positive control holds — #14143 itself and the runAs/automation-elevation family (#14201, #14011, #5494, #7900, #3783, #4366) came back, so the query matched semantically rather than returning a silent zero. Nearest neighbours excluded:
⛔ No duplicate.
Repro sketch
Declare a type: 'flow' action on an object with OWD private, targeting a flow with runAs: 'system'. Invoke it as a caller who cannot read the target row, with that row's id. The run's AutomationContext.record should be observed as { id: <recordId> } rather than absent — identical to a record-less start.
Observed while implementing #14143 on
66ecc50a. ⛔ Unclaimed. Filed rather than fixed: #14143's dispatch order binds that card to the handler predicate, and this is one surface over.What #14143 fixed, and where it stops
#14143 adds
ctx.recordLoadDeniedto the script/body action face: when the dispatcher's caller-scope load of the subject row does not deliver it, the handler is told, instead of only receivingctx.record = { id: recordId }(the id being stamped on precisely because the load failed).Both dispatch doors now route their load through one producer,
loadActionSubjectRecord(packages/runtime/src/action-execution.ts). But the flow branch consumes the samerecordobject and carries no equivalent signal:packages/runtime/src/action-execution.ts— MCPrun_action, theaction.type === 'flow'branch passesrecordintodispatchFlowAction;packages/runtime/src/domains/actions.ts— REST/actions, same;dispatchFlowActionthen hands it toautomation.execute(action.target, { record, ... })as the run'sAutomationContext.record, and seedsparamsfrom the same object viaseedFlowActionParams.So a flow started by a row-scoped action on a row its invoker cannot read receives
record = { id: <recordId> }— shaped exactly like a legitimate record-less start, with nothing on the context distinguishing the two.Why it is a smaller claim than #14143, not the same one
A flow action is deliberately not system-elevated: the flow engine receives the caller's identity and honours
runAs(ADR-0049), so arunAs: 'user'flow re-derives the caller's scope on its own reads and the stub simply resolves to nothing. The exposure is narrower and conditional:runAs: 'system'on the target flow gets elevation plus a record stub the invoker could not read, and no context key to guard on;record.idbeing present reads the same always-true predicate action dispatcher stampsctx.record.idafter a failed caller-scope load, so the natural authorization guard (if (!ctx.record?.id) refuse()) is always true on a row the caller cannot read #14143 removed from the handler face.⛔ Not claimed
ctx.record.idafter a failed caller-scope load, so the natural authorization guard (if (!ctx.record?.id) refuse()) is always true on a row the caller cannot read #14143: this is a predicate/observability gap, not an incident report.runAs: 'system'is wrong — it is a declared, documented authoring decision.AutomationContextkey mirroringrecordLoadDenied, a documentation change onrunAs: 'system', or nothing at all is a design question for triage; action dispatcher stampsctx.record.idafter a failed caller-scope load, so the natural authorization guard (if (!ctx.record?.id) refuse()) is always true on a row the caller cannot read #14143 ruled only the handler face and ruled the narrowing direction out of that card's scope.Dedupe
One targeted
search_issues(repo-scoped REST search answered 403 for this seat, so the channel switch is declared here). 11 results, positive control holds — #14143 itself and therunAs/automation-elevation family (#14201, #14011, #5494, #7900, #3783, #4366) came back, so the query matched semantically rather than returning a silent zero. Nearest neighbours excluded:flow-update-readonly-when-fieldskipsrunAs:'system'flows entirely, but the conditional strip has noisSystemexemption #14201 (open) —flow-update-readonly-when-fieldskipsrunAs: 'system'flows in lint. SamerunAsvocabulary, different site (a build-time rule, not the dispatcher's context assembly)./automationrun-detail returns the triggering record's fields without that record's own FLS. Adjacent in spirit; different surface (a read API over stored runs, not the record handed to a run at start).create_recordunderrunAs:'system'inserts rows withowner_id/organization_id/created_byall NULL — records born untouchable even by admin #5494 / [automation/audit] runAs:'system' 流回写的审计行无归因(user_id/actor 双空),console 历史显示「未知用户」 #4366 / Approvals status mirror drops the acting user, forcing every downstream record-change flow torunAs:'system'#3783 — attribution of what arunAs: 'system'run writes. This is about what such a run is given.⛔ No duplicate.
Repro sketch
Declare a
type: 'flow'action on an object with OWDprivate, targeting a flow withrunAs: 'system'. Invoke it as a caller who cannot read the target row, with that row's id. The run'sAutomationContext.recordshould be observed as{ id: <recordId> }rather than absent — identical to a record-less start.