Repository navigation
Commit 96e7244
fix(runtime): mount POST /automation/:name/clone on the dispatcher bridge, at both bases (#20779)
Fixes #20676
Clause-②: no
## What was broken
ADR-0126 §7.1's flow clone door answered `404 ENDPOINT_NOT_FOUND` on
every live server, for every caller and every body. The domain arm
(`packages/runtime/src/domains/automation.ts`, `POST /:name/clone`)
exists, but `registerAutomationRoutes` in
`packages/runtime/src/dispatcher-plugin.ts` mounts every `/automation`
route explicitly and never mounted this one, so the transport's
`notFound` answered before `dispatch()` ran. The arm's unit test
(`domains/automation-flow-clone.test.ts`) stayed green because it drives
`HttpDispatcher` directly, below the mount. The route was also missing
from `route-ledger.ts`, so the live-mount parity gate had no row to
flag.
## What changed
- `packages/runtime/src/dispatcher-plugin.ts`: `POST
${base}/automation/:name/clone` mounted beside `/:name/toggle`,
dispatching to `POST /automation/:name/clone`.
`registerAutomationRoutes` runs for both bases, so the
environment-scoped twin
(`/api/v1/environments/:environmentId/automation/:name/clone`) is
mounted by the same line. Registered after `trigger/:name`: for a flow
literally named `clone`, `POST /automation/trigger/clone` still reaches
the legacy execution door, and either mount rebuilds the identical
dispatch path, which the domain answers `trigger` first.
- `packages/runtime/src/route-ledger.ts`: a `POST
/automation/:name/clone` row, `server-only`, with its rationale (the
operational driver is the Setup page, which calls the platform API
directly; the same posture as the `POST
/actions/_activation/:object/:action` row). There is no
`client.automation.clone` SDK method, and `gap` is ratcheted at 0.
Census regenerated with `--fix`: 81 to 82 rows.
- `.changeset/20676-mount-flow-clone.md`: `@objectstack/runtime` patch.
No domain arm, gate, response shape or spec file changed.
`packages/runtime/src/domains/automation.ts` is untouched.
## Sweep: domain arms against bridge mounts
Every `handleAutomationRequest` arm, diffed against the
`registerAutomationRoutes` mounts on `origin/main` `f284ab26`:
| Domain arm | Bridge mount | Verdict |
|---|---|---|
| `POST /` (create) | `POST /automation` | mounted |
| `GET /actions`, `GET /connectors`, `GET /_status` | the three literal
mounts, before `/:name` | mounted |
| `GET /:name`, `PUT /:name`, `DELETE /:name` | `/automation/:name` x3 |
mounted |
| `POST /trigger/:name` (legacy) | `/automation/trigger/:name` | mounted
|
| `POST /:name/trigger` | `/automation/:name/trigger` | mounted |
| `POST /:name/toggle` | `/automation/:name/toggle` | mounted |
| **`POST /:name/clone`** | none | **mounted by this PR** |
| `GET /:name/runs`, `GET /:name/runs/:runId` | both mounted | mounted |
| `POST /:name/runs/:runId/resume` | mounted | mounted |
| `POST /:name/runs/:runId/cancel`, `/restore-suspension` | both mounted
| mounted |
| `GET /:name/runs/:runId/screen` | mounted | mounted |
| `GET /` (flow list) | none | retired (#19543 door 4), correctly
unmounted |
The clone door was the only unmounted arm. No undeclared door was found,
so nothing was mounted beyond the card.
## Pins
- `packages/qa/dogfood/test/automation-flow-clone-door.dogfood.test.ts`
boots the CRM app with the automation service through `bootStack` (the
real Hono app) and clones the shipped `crm_convert_lead_wizard`. It pins
these cases:
- an anonymous caller gets `401 UNAUTHENTICATED` (the domain floor, not
the transport 404);
- a legal clone gets `200` with `data.notice === FLOW_CLONE_NOTICE`
(imported, not restated) and `status: 'draft'`, and the clone reads back
on `GET /automation/:name`;
- an illegal machine name gets `400 VALIDATION_FAILED`, and nothing is
registered under it;
- a missing `name` gets `400 VALIDATION_FAILED`;
- a taken name gets `409 RESOURCE_CONFLICT`.
-
`packages/runtime/src/dispatcher-plugin.automation-clone-mount.integration.test.ts`
covers the environment-scoped twin, which `bootStack` never mounts
because it boots without project scoping. It uses `plugin-hono-server`
and the dispatcher with `enableProjectScoping: true` over a real socket.
The discriminator is the anonymous floor's `401 UNAUTHENTICATED`, which
only the dispatcher mints. Both bases are probed, with a positive
control (`/:name/trigger`, changed from `/:name/toggle` in patch round 1
so it holds whichever of this PR and PR #20780 lands first) and a
negative control (an unmounted sibling segment answering the transport
404).
- With the row in the ledger,
`route-ledger-live-mount-parity.dogfood.test.ts` now also guards this
mount.
## Reverse verification (ablation, one-off, nothing left in the tree)
The fix was committed first. `scripts/ablation-replace.mjs` then renamed
the mount path (`automation/:name/clone` became
`automation/:name/clone-ablated-20676`, anchor 1 to 0). Runtime was
rebuilt, and `ablation-dist-preflight.mjs` found the marker in
`dist/index.js` and `dist/index.cjs`.
- The runtime pin went red on the 2 clone cases, each with `404
{"code":"ENDPOINT_NOT_FOUND"}`. Both controls stayed green.
- The dogfood pin went red on 5 of 5 cases, each with `404
ENDPOINT_NOT_FOUND`, the card's own symptom byte for byte.
- The ledger parity gate went red on 2 cases: `POST
/automation/:name/clone — LEDGERED BUT NOT MOUNTED`, and the ablated
mount unledgered.
Restore: the blob equals the HEAD blob (`b6dc62c9`), whole-tree `git
status --porcelain` is empty, runtime was rebuilt, and `--absent`
preflight shows the marker absent from all 6 built files. Re-run:
runtime pin 4/4 green; dogfood (the clone pin, the ledger parity gate
and `automation-toggle-tenant-scope`) 21/21 green.
## Downstream prose this makes true
- The `FLOW_DISABLED` refusal ("...or run a clone of it under a new
name", `service-automation/src/engine.ts`) and the Setup page copy now
point at a door that answers.
- `content/docs/capabilities/integrations.mdx` promises "switch it off
and clone your own to edit in Studio". The clone half is now true. The
**edit in Studio** half is not, as measured on the same harness, one-off
and not committed:
- after a `200` clone, `GET /api/v1/meta/flow/CLONE` answers `404
RESOURCE_NOT_FOUND`, while the source answers `200`;
- after a cold boot on the same database file, `GET
/api/v1/automation/CLONE` answers `404`, while the source answers `200`.
The clone is engine-only. That is FOLLOW-UPS §8a D18, outside this card,
and not fixed here. Carrier: #20761's stage 2. The maintainer's ruling
there (`5904938166`) pins "a clone of a shipped flow is saved as a
tenant row", and the seat has posted this measurement on that card.
## Acceptance notes
- **Fixed here** (a bounded in-place fix, patch round 1): the
`/automation` enforcement prose in
`packages/qa/dogfood/test/authz-conformance.matrix.ts` said "four gated
flow writes". It now names five, adding the ADR-0126 §7.1 clone `POST
/:name/clone`. Evidence: `isFlowAuthoringWrite` in
`packages/runtime/src/domains/automation.ts` returns true for exactly
five route shapes: `POST /` (`parts.length` 0), and `POST
/:name/toggle`, `POST /:name/clone`, `PUT /:name` and `DELETE /:name`
(`parts.length` 1).
- **Fixed here** (a bounded in-place fix, patch round 1): the note on
`route-ledger.ts`'s `POST /automation/:name/toggle` row.
- BEFORE: 'The enabled bit is not a ROW, so no organization wall scopes
it: `toggleFlow` writes an in-process map keyed by flow name only,
`getFlowRuntimeStates()` reads it with no caller and no organization,
and the automation service is ONE instance per environment'.
- AFTER: 'No organization wall scopes the enabled bit: `toggleFlow`
writes the ADR-0126 §7.2 activation ledger first — one deployment-wide
`sys_metadata_activation` row per flow, keyed by `(metadata_type,
name)`, carrying the flow's package id and no organization column — and
only then updates the engine's in-process projection, which
`getFlowRuntimeStates()` reads with no caller and no organization; the
automation service is ONE instance per environment'.
- Evidence: `toggleFlow` in `service-automation`'s `engine.ts` calls
`flowActivationStore.setActive` before it updates `flowLedgerDisabled`,
and core's `metadata-activation-store.ts` has the columns
`metadata_type`, `name`, `package_id` and `active`, matched on
`(metadata_type, name)`.
- "Packaged flows only" is NOT added here: that is PR #20780's
behaviour, and whichever of the two PRs lands second adds it.
- The dogfood census pins were re-derived by the census file's own
method (patch round 1). In `authz-probe-blind-spot.census.ts`, the
`route-ledger.ts` probe row went from population/reach 81/81 to 82/82,
with the blind spot 0 and keys 21 unchanged; `BLIND_SPOT_TOTAL_STATIC`
67 / `_RUNTIME` 72 are unchanged. The `authz-conformance.matrix.ts`
docblock now reads (82 rows / 21 domains).
- `#20679` (the packaged-flow lock on PUT/DELETE) is not addressed here.
The clone pins use a new, customer-owned name and do not exercise that
lock.
- `docs/qa/platform-checklist/FOLLOW-UPS.md` §8a D22 ("`POST
/automation/:name/clone` is unledgered") goes stale when this lands. The
file is outside this card's surface. Carrier: none; noted, not filed.
## Verification
**Patch round 1 — final head `99b3cfa4`** (a merge of `origin/main` over
`ab7d5015` and `5518c808`):
- Runtime pins (`dispatcher-plugin.automation-clone-mount.integration`,
`route-ledger.conformance`, `automation-api-contract-mounts`,
`domains/automation-flow-clone`): 4 files, 31 tests passed. Runtime and
dogfood typecheck are green.
- The full dogfood package: 141 files passed and 1 skipped (142); 1155
tests passed and 3 skipped. The two formerly red files
(`authz-conformance.test.ts`, and `authz-probe-blind-spot.test.ts`, the
shard-3 file) pass.
- `dispatch-gates --ran` reconciles 67 of 67 with 0 NOT-MEASURED.
`check:route-ledger-census` reads 82, and the array holds 82.
**Round 0 (head `0b1c343e`), kept for the record:**
Head `0b1c343e`. The full runtime suite ran at `d663c2fe`, whose only
difference from `0b1c343e` is one string in the new ledger note. Every
suite that reads the ledger was re-run at `0b1c343e`.
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2` at `d663c2fe`: 291 files passed, 4204 tests passed, 1
skipped.
- At `0b1c343e`: `route-ledger.conformance`,
`automation-api-contract-mounts`, the new clone-mount pin and
`domains/automation-flow-clone`, 4 files and 31 tests passed. Dogfood:
the clone pin and the ledger parity gate, 13/13 passed.
- `pnpm --filter @objectstack/runtime typecheck` (`tsc --noEmit` plus
`check:test-typecheck`) and `pnpm --filter @objectstack/dogfood
typecheck`: both green at `0b1c343e`. `tsc --listFiles` confirms each
program contains its new test file (1 hit each).
- `dispatch-gates --commands` (67 commands) at `d663c2fe`: 64 exited 0.
The other three were resolved as follows:
- `check:doc-authoring` was a real finding: a tracker id inside the new
ledger note string. It is removed in `0b1c343e`, and the gate now exits
0.
- `check-plugin-teardown-shape --self-test` refused on the shallow
clone. After fetching its pinned fixture commit it passed, 48 cases.
- `check:dual-build-cjs-loads` refused with a prerequisite error: 8
packages outside the build closure had no `dist/`. They are now built.
The final-head re-run of all 67, and the `--ran` reconciliation, are in
the report comment on the card.
- Lint, as a proven narrowing and not a full `pnpm lint`. eslint
`--format json` over the 4 touched TS files returned 4 results, 0
errors, 0 warnings. Each file is inside eslint's own population
(`--print-config` returns 6/6/5/5 rules). `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`, no `projectService`),
and its only file reads are two baseline JSONs this diff does not touch,
so the diff cannot move any untouched file's verdict. The full-tree
`pnpm lint` is left to CI.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 41dcf11 commit 96e7244
7 files changed
Lines changed: 318 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
244 | | - | |
| 244 | + | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
362 | 362 | | |
363 | 363 | | |
364 | 364 | | |
365 | | - | |
366 | | - | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
367 | 371 | | |
368 | 372 | | |
369 | | - | |
| 373 | + | |
370 | 374 | | |
371 | 375 | | |
372 | 376 | | |
| |||
Lines changed: 141 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
Lines changed: 117 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
0 commit comments