You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f1ddace
Browse filesBrowse the repository at this point in the historyBrowse files
feat(automation): flow-level requiredPermissions, checked at every flow start door (#25643)
Fixes#25580
Ruling-ref: 6377848497
Clause-②: yes (widening)
Metadata-protocol: added
## What
A flow can now declare who may START it:
`FlowSchema.requiredPermissions: string[]`, the same value shape as an
action's `requiredPermissions` (ADR-0066 D4). Ruling A's order holds
inside this one PR (retriage answer 6382852102): the spec adds the key,
and the server checks it in the one shared check every start door asks.
### The shared check —
`packages/runtime/src/flow-start-admission.ts#flowStartRefusal`
The module already existed as "the ONE definition asked by every door
that starts a flow by name" (the elevated self-triggered rule). Its
door-facing predicate is replaced by one function,
`flowStartRefusal(automation, flowName, executionContext)`, which
returns the ADR-0112 refusal or `null`:
1. **Elevated self-triggered rule** (unchanged): a non-system caller may
not start a `runAs: 'system'` flow of type `autolaunched` /
`record_change` / `schedule`. Answers first, because no capability opens
it.
2. **The flow's `requiredPermissions`** (new): a non-system caller must
hold every named capability, or `403 PERMISSION_DENIED`, nothing
dispatched. The capability arithmetic is
`packages/runtime/src/required-capabilities.ts#missingCapabilities`,
which the action gate `actionPermissionError` now asks too: one spelling
of "holds X" for both keys.
The system principal (`isSystem`) is admitted, the action gate's bypass;
the census row on `content/docs/permissions/system-context.mdx` is
re-anchored to the new symbol.
### The doors (each one call to the check, nothing else)
| Door | Call site |
|---|---|
| Trigger route `POST /automation/:name/trigger` (and the legacy
spelling) |
`packages/runtime/src/domains/automation.ts#respondToFlowTrigger` |
| Action of `type: 'flow'`: REST `POST /actions/:object/:action` and MCP
`run_action` |
`packages/runtime/src/action-execution.ts#dispatchFlowAction` (both
entrances reach it) |
| Declared endpoint of `type: 'flow'` |
`packages/runtime/src/endpoint-executor.ts#executeFlow` |
`packages/mcp` needed no change: `run_action` reaches
`dispatchFlowAction` through the runtime bridge.
### Decisions recorded
- **The action door inherits the flow's list.** An action that starts a
gated flow is gated by it even when the action declares none.
- **When both declare, the caller needs both lists.** The action's own
gate answers first (upstream, unchanged). The flow's gate then answers
at `dispatchFlowAction`.
- **Sub-flows are not doors.** A parent's `subflow` node starts the
child through `engine.execute`, which never reads the key. Pinned on the
real engine.
- **The declared endpoint is gated too.** The ruling names three doors.
The existing shared module already treats `type: 'flow'` endpoints as a
fourth door that starts a flow by name, so leaving it out would leave a
bypass.
- **The refusal names the declared and the missing capabilities** (as
the action gate's does) and nothing else about the flow.
- **`runAs: 'user'` flows are gated too.** The key gates the start, not
the elevation.
### Authoring surfaces
- `os validate`: `capability-reference-unknown` now judges
`flows[i].requiredPermissions` through the same code that judges actions
(`packages/lint/src/validate-capability-references.ts`), and `os
explain` names the position.
- Studio flow form: offers the key (`string-tags`, as on the action
form). The translation bundles are regenerated; zh-CN, ja-JP and es-ES
are translated by hand.
- Generated: authorable-surface shards, references docs, liveness row
(`live`) and state counts.
## Tests
- `packages/runtime/src/flow-start-required-permissions.test.ts` covers
all four doors (trigger, REST `/actions`, MCP `run_action`, endpoint).
Each case is pinned on `code` + `status` + never dispatched:
- without X → `403 PERMISSION_DENIED`; with X → starts
- EVERY capability required; system principal admitted; absent or empty
list admits
- the elevated self-triggered rule answers first
- identical envelope at all four doors
- inheritance with no action list; both-declared in each direction
-
`packages/services/service-automation/src/subflow-required-permissions.test.ts`:
a gated child runs from its parent for a member holding nothing, on the
real engine.
- `packages/spec/src/automation/flow-required-permissions.test.ts`: full
parse green and kept on the parsed definition; wrong value shapes
refused at the same paths the action key refuses.
- `packages/lint/src/validate-capability-references.test.ts`: a typo at
`flows[1].requiredPermissions` warns; a granted one passes.
Readings are on head `3ddf0535` (and as noted per line) (local subset;
CI runs the farm):
- `@objectstack/runtime` full suite (pre-merge head `a2fd696f`; the
merge brought no runtime change): Test Files 441 passed (441), Tests
7080 passed / 19 skipped. On the merged head `86714f38`: the three door
pin files, 97 passed.
- `@objectstack/spec` full suite on `86714f38`: Test Files 716 passed
(716), Tests 21712 passed / 1 todo. The first run, pre-merge, was 2
failed (the translation-face and form-reconciliation pins named the new
key); both were fixed in this PR by offering the key in the flow form
and adding a translation-face row.
- `@objectstack/lint` full suite on `86714f38`: Test Files 159 passed
(159), Tests 7245 passed.
- `@objectstack/service-automation` full suite (pre-merge): Test Files
200 passed (200), Tests 2630 passed. The sub-flow pin on `86714f38`: 2
passed.
- `@objectstack/platform-objects` full suite on `3ddf0535`: Test Files
94 passed (94), Tests 1343 passed. The catalog count pin moved 678 to
679 for the new form row.
- typecheck on the merged head: spec, runtime, service-automation, lint,
rest and platform-objects all `Done`, exit 0.
Ablation (one-off, no permanent file). Mutating `if (missing.length > 0)
return requiredPermissionsRefusal(` to `if (false) …` through
`scripts/ablation-replace.mjs` reds
`flow-start-required-permissions.test.ts`: 21 failed / 24 passed of 45.
The file was restored byte-identical (blob `9f81b3b1570b` before =
after; `git diff HEAD` empty). The runtime tests import `src`
relatively, so no `dist/` leg was needed.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, which gives 122 commands.
- All 122 ran on head `86714f38` (after the `origin/main` merge), each
exit code captured before any pipe: 122 exit 0.
- `check-plugin-teardown-shape --self-test` first answered 3 (shallow
clone). After fetching its pinned fixture commit it answered 0.
- The last commit (`3ddf0535`) only touches a platform-objects test
file. It re-derives the same 122, and the ratchet families on that head
answered 0: test-source-alias, cross-package-test-inputs, i18n,
i18n-stale-fill, type-check-debt, type-check-coverage.
- `--ran` reconciliation: 122 derived, 122 run, 0 NOT-MEASURED, 0 UNRUN,
exit 0.
- Also local: `check-system-context-census` OK;
`check-platform-checklist` OK, and `--changed-from origin/main` OK with
0 anchors lost; `check:dev-prereqs` OK; `check-nul-bytes` OK; spec
`check:generated` "All 14 generated artifacts are up to date" after the
merge.
- Not run locally: full dogfood. There is no `!` in the title and no
`(narrowing)` arm. CI runs the farm.
## Producers
Measured at `7265b33e77`: 8 flows in `examples/**` declare `runAs:
'system'` (showcase 6, todo 2). 2 of them are `screen` / `api`:
`showcase_inquiry_purge` (screen) and `showcase_inbound_task_webhook`
(api). None declares `requiredPermissions`, so none changes behaviour.
`packages/**` holds no flow definition that declares `runAs: 'system'`
outside tests.
## Acceptance notes
- **Listing faces do not mirror the inherited gate.** Two hide faces use
only the action's own `requiredPermissions`: MCP `list_actions`
(`packages/runtime/src/domains/mcp.ts`, the `actionPermissionError`
filter) and the `/meta` action strip. So a member can still see an
action whose target flow refuses them, and running it answers 403. These
are UI conveniences, not access boundaries, and the doors enforce. Not
filed: it is an observation, not one of the three filing classes.
- **Not start doors:** `resume_run` / the screen-resume route continue a
run that a door already admitted, so they are not checked again.
- **Pre-existing, unchanged:** at the MCP bridge, the action's OWN
`requiredPermissions` refusal is thrown as a bare sentence with no
`code`/`status`, as the existing elevated-door pins already record. The
new flow-level refusal carries `PERMISSION_DENIED`/403 at both
entrances.
- **Files beyond the claim's declared surface:** each is either a door
itself or a gate the new key forces.
- Doors: `runtime/src/domains/automation.ts` (the trigger door lives
there, not in `action-execution.ts`) and
`runtime/src/endpoint-executor.ts`.
- New runtime files: `runtime/src/required-capabilities.ts` and the door
pin.
- Forced by gates: the `service-automation` sub-flow pin;
`spec/src/automation/flow.form.ts` and
`spec/src/system/flows-translation-face.test.ts` (form reconciliation
and translation-face pins); `platform-objects` translation bundles;
`spec/liveness/flow.json`; `content/docs/permissions/system-context.mdx`
(census); `docs/qa/platform-checklist/areas/automation.json` (the anchor
followed the renamed symbol).
- Comment-only re-points to the new symbol name:
`rest/src/import-prepare.ts` and
`spec/src/api/endpoint-publish-gate.ts`.
- **hotsrm** can drop its `current_user.positions` gate in
`srm_bank_verify` once this ships. The four-eyes half (the creator may
not verify) is out of scope per the ruling.
Session: `session_01HEB7eZUjj5begLB9coGhFs`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(automation): a flow declares who may start it — `requiredPermissions` on the flow, checked at every start door
9
+
10
+
A flow can now carry `requiredPermissions: ['capability', …]`, the same value shape as an action's `requiredPermissions` (ADR-0066 D4). Every door that starts a flow by name checks it in one shared check: the trigger route (`POST /automation/:name/trigger`), an action of `type: 'flow'` (REST `POST /actions/:object/:action` and the MCP `run_action` bridge), and a declared endpoint of `type: 'flow'`. A caller that is not the system principal must hold every named capability (granted through a permission set's `systemPermissions`), or the door answers `403 PERMISSION_DENIED` and nothing runs.
11
+
12
+
-**The action door inherits the flow's list.** An action that starts a gated flow is gated by the flow even when the action declares nothing, so the app states it once. An action that declares its own list keeps its own gate, which answers first, so a caller at that door needs both lists.
13
+
-**A sub-flow is not a door.** A parent flow's `subflow` node starts its child through the engine and is not checked again; the parent's own start was.
14
+
-**The elevated self-triggered rule still answers first.** Holding the capabilities does not let a member start a `runAs: 'system'` flow of type `autolaunched`, `record_change` or `schedule`.
15
+
-**Studio's flow form** offers the key (a tag list beside `runAs`).
16
+
-**`os validate`** judges the key's values the way it judges an action's: a capability no built-in, `defineCapability` or `systemPermissions` grant provides is reported as `capability-reference-unknown` at `flows[i].requiredPermissions` (`os explain capability-reference-unknown`).
17
+
18
+
This is how to say who may start an elevated (`runAs: 'system'`) `screen` or `api` flow. An app that gates such a flow with a `decision` node on `current_user.positions` can move that gate to the flow's `requiredPermissions`; the positions array is not an authorization input. A flow that declares no `requiredPermissions` starts exactly as before.
19
+
20
+
Clause-②: yes (widening) — a flow may declare a new key, `requiredPermissions`; a flow that does not declare it is admitted at every door exactly as before.
Copy file name to clipboardExpand all lines: content/docs/api/declarative-endpoints.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -101,7 +101,7 @@ declaration to shadow a built-in one. Match → policy chain (`rateLimit` → `a
101
101
|`type`| Delegates to | Request shape |
102
102
|:---|:---|:---|
103
103
|`object_operation`| the same `callData` binding that serves `/api/v1/data/{object}`|`find` reads its criteria from the query string; `get` / `update` / `delete` take the record id from `query.id`; `create` / `update` take the body. `create` answers `201`, the rest `200`|
104
-
|`flow`| the same automation pipeline as `POST /api/v1/automation/{name}/trigger`| the request body is the flow input. A refused or failed run answers a real status: `404` unknown flow, `409``FLOW_DISABLED`, `422``FLOW_NO_START_NODE`, `422``FLOW_INPUT_SCHEMA_INVALID`, `400``FLOW_FAILED`, and `403``PERMISSION_DENIED`, nothing run, when the target is declared `runAs: 'system'` with a self-triggered type (`autolaunched`, `record_change`, `schedule`), exactly as the trigger door answers |
104
+
|`flow`| the same automation pipeline as `POST /api/v1/automation/{name}/trigger`| the request body is the flow input. A refused or failed run answers a real status: `404` unknown flow, `409``FLOW_DISABLED`, `422``FLOW_NO_START_NODE`, `422``FLOW_INPUT_SCHEMA_INVALID`, `400``FLOW_FAILED`, and `403``PERMISSION_DENIED`, nothing run, when the target is declared `runAs: 'system'` with a self-triggered type (`autolaunched`, `record_change`, `schedule`) or declares `requiredPermissions` the caller does not hold, exactly as the trigger door answers |
105
105
106
106
The frozen vocabulary has **no path-template syntax**, so an endpoint cannot express
107
107
`/leads/{id}` — a record id travels as `?id=…`. A method that no declaration claims
Copy file name to clipboardExpand all lines: content/docs/automation/flows.mdx
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,6 +74,7 @@ const approvalFlow = {
74
74
|`status`|`enum`| optional |`'draft'`, `'active'`, `'obsolete'`, `'invalid'` (defaults to `'draft'`) |
75
75
|`type`|`FlowType`| ✅ | Flow trigger type (see below) |
76
76
|`runAs`|`enum`| optional |`'system'` (elevated, bypasses RLS; after an approval decision its attribution user is the decider) or `'user'` (the default, RLS-respecting: the trigger user until a human decision, the decider after it, with the decider's permissions). A `'user'` run that resolved **no** trigger user has nothing to scope to, so its data operations are refused — declare `'system'` for schedule / time-relative / API triggers and for record-change flows fired by a write that carried no user |
77
+
|`requiredPermissions`|`string[]`| optional | Capabilities a caller must hold, every one listed, to START the flow. Checked at every door that starts a flow by name (the trigger route, a `type: 'flow'` action over REST and MCP `run_action`, a declared `type: 'flow'` endpoint): anyone else but the system principal is refused `403``PERMISSION_DENIED` and nothing runs. An action that starts the flow inherits the list. A parent flow's `subflow` node is not a door and is not checked again. This is how to say who may start an elevated `screen` or `api` flow; `current_user.positions` is not an authorization input |
|`edges`|`FlowEdge[]`| ✅ | Connections between nodes |
@@ -2291,20 +2292,21 @@ curl -b cookies.txt -X POST \
2291
2292
|`POST /api/v1/automation/:name/runs/:runId/restore-suspension`| Re-arm a run stranded by a failed resume — and, for a nested run, every ancestor whose pause that strand's cascade consumed, deepest first, from whichever member you name. Same gate as `cancel`; body `{ reason? }`. `501 NOT_IMPLEMENTED` when the service does not declare the verb |
2292
2293
2293
2294
**Failures answer real HTTP status codes.** A flow that does not run is not a
2294
-
`200`. The route classifies six outcomes. Five of them it reads off the engine's
2295
-
own verdict — never the message text; the `403` is the door's own admission
2295
+
`200`. The route classifies seven outcomes. Five of them it reads off the engine's
2296
+
own verdict — never the message text; the two `403`s are the door's own admission
2296
2297
check, made before anything is dispatched:
2297
2298
2298
2299
| Status |`error.code`| What happened | What to do |
2299
2300
|:---|:---|:---|:---|
2300
2301
|`404`| — | No flow by that name in this deployment | Check the name; nothing was dispatched |
2301
2302
|`403`|`PERMISSION_DENIED`| The caller is not the system principal, and the flow is declared `runAs: 'system'` with a self-triggered `type` (`autolaunched`, `record_change`, `schedule`). The same answer at every door that starts a flow by name: this route, a `type: 'flow'` action (REST and MCP `run_action`) and a declared `type: 'flow'` endpoint | Let it run on its own trigger, or call it from a parent flow's `subflow` node. If members are meant to start it, declare it `type: 'screen'` (or `'api'` for a signed inbound hook) |
2303
+
|`403`|`PERMISSION_DENIED`| The flow declares `requiredPermissions` and the caller, not the system principal, does not hold every capability it names. Checked after the row above, at the same doors; the message names the declared and the missing capabilities | Grant the capability through a permission set's `systemPermissions`, or start the flow as someone who holds it |
2302
2304
|`409`|`FLOW_DISABLED`| The flow exists but is switched off | Enable it — the identical request then succeeds |
2303
2305
|`422`|`FLOW_NO_START_NODE`| The stored definition has no `start` node | Fix the flow; retrying cannot help |
2304
2306
|`422`|`FLOW_INPUT_SCHEMA_INVALID`| A node's `config` violates the `inputSchema` its own definition declares | Fix the flow; the refusal is non-retryable — even under `errorHandling.strategy: 'retry'` the engine refuses once and never enters the retry loop |
2305
2307
|`400`|`FLOW_FAILED`| The flow **ran** and was rejected | Read `error.details.summary` for the failing node |
2306
2308
2307
-
The first five never dispatched anything: no node executed, no record was
2309
+
The first six never dispatched anything: no node executed, no record was
2308
2310
written, and there is no run to look up. Only `400` describes a run. The
2309
2311
`FLOW_INPUT_SCHEMA_INVALID` verdict is a pure function of the flow definition —
2310
2312
re-running the guard cannot change the answer — so a `retry` strategy does not
Copy file name to clipboardExpand all lines: content/docs/permissions/system-context.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -193,7 +193,7 @@ One of the largest consumers of the flag;
193
193
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability |`packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability`|
194
194
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability |`packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring`|
195
195
| 55b | Walled environment-metadata refusal does not fire | metadata-core, rest, runtime | Get: under `isolated` / `group`, a write of the environment's metadata, of any type, at the `/meta` write doors (save, reset, publish, rollback) and `POST /meta/_migrate-stored` on both transports, at every `/packages` write door (create, the manifest `PATCH`, enable, disable, publish, `publish-drafts`, `discard-drafts`, `revert`, `rollback`, `commits/:commitId/revert`, `duplicate`, `adopt-orphans`, delete), at the `/automation` definition writes (create, update, delete, clone), and at the permission-set clone door. Every other caller below the platform-operator rung is refused `403` there (ADR-0131 D3, D12 items 7–8, D11), because one organization's write would change what every organization resolves. The seeders, the package door and migrations write the environment's metadata this way. Under `single` the refusal is inert for every caller | `packages/metadata-core/src/meta-write-capability.ts#walledCatalogWriteVerdict`, `packages/rest/src/rest-server.ts#walledCatalogRefusal`, `packages/runtime/src/domains/packages.ts#refuseWalledCatalogPackageWrite`, `packages/runtime/src/domains/automation.ts#refuseWalledFlowDefinitionWrite` |
196
-
| 57 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A). The three doors ask one predicate, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart` |
196
+
| 57 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A) — and starting, through the same doors, a flow whose `requiredPermissions` the caller does not hold (ruling record 6377848497, letter A). The three doors ask one check, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `packages/runtime/src/flow-start-admission.ts#flowStartRefusal` |
197
197
| 58 | Audience-binding suggestion authority gate passes | plugin-security | Get: a system caller lists, accepts and dismisses suggestions without `manage_metadata` and, under a wall, without the platform-operator rung |`packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertDeploymentAuthority`|
198
198
| 59 | Email-template organization door passes; webhook provenance stamp skipped | plugin-email, plugin-webhooks | Get (`sys_email_template`): a create or update of a template row. The door refuses every other write that names a caller, `403 PERMISSION_DENIED` (ADR-0131 D6); the plugin itself no longer writes the table (templates resolve from the registry), so the v18 migration ceremony's promotion is the one remaining writer. Lose (`sys_webhook`): the row is not marked as an admin customization, and the declared-name guard does not run (a new row or a rename taking a declared webhook's name, 409) — the system writers are the platform's machinery (the legacy-secret sweep, the ownership ceremony, migrations) |`packages/plugins/plugin-email/src/email-template-door.ts#isOrganizationWrite`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp`, `packages/plugins/plugin-webhooks/src/declared-webhooks.ts#bindDeclaredWebhookNameGuard`|
199
199
| 60 |**Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win |`packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes`|
0 commit comments