Repository navigation
Commit 76bd58f
Fixes #20761
Clause-②: yes (widening)
## What this does
Stage 2 of #20761, under the maintainer's ruling B + A recorded in
`5904938166`. The platform now trusts only a fact it holds itself about
which flows are packaged. For flows, "packaged" means exactly "loaded by
the loader from a managed package".
1. **The engine reads the loader's set, not the body (ruling point 1).**
The automation engine no longer classifies a flow by the provenance
stamps its own definition carries. Every classification reads one reader
over the loader's set: the ADR-0126 §7.3 subflow guards in both
directions, the arming gate, the activation door, and the package an
activation row is attributed to. The automation plugin attaches that
reader. It asks the metadata protocol at question time, with nothing
cached at boot, so it follows the registry across install, upgrade and
reload exactly as the locked-base verdict does. The body's stamps are
kept for display only.
2. **One shared rule on every flow write door (ruling point 2).** It
lives in `@objectstack/metadata-protocol`, beside the #20679 locked-base
verdict. It is asked by the automation create door, the update door, the
clone door and the `/meta` flow write.
- A name the loader's set holds is refused as a locked base. That is
#20679's `packagedBaseRefusal`, reused, not re-implemented. So a round
trip of a shipped flow is refused.
- Any other name, with a body whose stamps would classify it as
code-shipped, is refused loudly: `422 INVALID_METADATA`, a code already
registered to this package in the ADR-0112 ledger. No new code is
minted. Nothing is written or registered.
- A tenant row's own stamps, echoed on a round trip, agree with the
server's fact and are a no-op (see "Round trips" below for the one case
this had to measure).
3. **Clone (ruling point 3).** The copy still drops the base's whole
protection envelope. `flow-clone.ts` already did that, and the envelope
drop is pinned in `automation-flow-clone.test.ts`. The copy is now also
judged by the same rule and **saved as a tenant row** through the
metadata protocol's own save, env-wide. It reads back on the metadata
door and survives a cold boot. The engine registers the copy first and
the store saves it second, and a save that fails withdraws the
registration, so no clone is reported that would not survive.
4. **Scope (ruling point 5).** `/meta`'s handling of every other
metadata type is unchanged: the rule answers nothing for them. The two
server-stated rewrites of stored rows (stored-metadata migration,
package duplication) are not authoring doors and keep their old
handling.
## Declared cross-lane touch
The card is `domain:cli`, and its entry is the automation doors in
`packages/runtime`. The claim (`5909772878`) declares two cross-lane
surfaces, and this PR stays inside them:
- `domain:engine`: `metadata-protocol`
(`packages/metadata-protocol/src/**`). The shared rule and the
loader's-set read, both new public members, and `/meta`'s flow write
applying the rule.
- `domain:services`: `service-automation`
(`packages/services/service-automation/src/**`). The engine's
classification reads the loader's set, and the plugin wiring hands it
over.
Nothing under `packages/spec/**`, `packages/objectql/**` or
`packages/metadata-core/**` is touched.
## The seam
| role | file | symbol |
|:--|:--|:--|
| loader's-set read | `packages/metadata-protocol/src/protocol.ts` |
`ObjectStackProtocolImplementation.packagedArtifactOwner` |
| the one authoring rule | `packages/metadata-protocol/src/protocol.ts`
| `ObjectStackProtocolImplementation.tenantAuthoredWriteRefusal` (the
lock branch calls `packagedBaseRefusal`) |
| `/meta` applies it | `packages/metadata-protocol/src/protocol.ts` |
`saveMetaItem`, before the stamps are stripped |
| automation doors apply it |
`packages/runtime/src/domains/automation.ts` |
`refuseUnauthoredFlowWrite` (`POST /`, `PUT /:name`, clone) |
| clone saved as a tenant row |
`packages/runtime/src/domains/automation.ts` | clone arm →
`protocol.saveMetaItem` |
| engine reads the set |
`packages/services/service-automation/src/engine.ts` |
`setPackagedFlowSource`, `packagedFlowOwner` (every former body-stamp
site) |
| wiring | `packages/services/service-automation/src/plugin.ts` |
`packagedFlowReader` |
## Pins (ruling point 4)
| pin | where |
|:--|:--|
| a disagreeing assertion is refused on create and on update (`422
INVALID_METADATA`), nothing registered or written | runtime
`automation-tenant-authored-write.test.ts`; dogfood |
| a round trip of a shipped flow is refused as a locked base (`403
NOT_OVERRIDABLE`) on both doors | runtime; metadata-protocol
`protocol.tenant-authored-write.test.ts`; dogfood |
| a clone of a shipped flow is saved as a tenant row: it reads back on
the metadata door and survives a cold boot on the same database file |
runtime (the save and its rollback); dogfood (read-back and cold boot) |
| a round trip of a customer flow is accepted unchanged, on both doors |
runtime; metadata-protocol; dogfood |
| the §7.3 guards and the toggle door treat a customer flow as
customer-authored, whatever its body's stamps say | service-automation
`packaged-flow-source.test.ts`; dogfood |
| no activation row is attributed to a package from an authoring path |
service-automation (a row is attributed to the package the set names,
never the one the stamps name); dogfood (ledger read, no row) |
| `/meta`'s flow write applies the same rule | metadata-protocol;
dogfood |
| control: `/meta` on another type keeps its old handling |
metadata-protocol; dogfood |
At least one pin runs through the real HTTP composition:
`packages/qa/dogfood/test/flow-provenance-server-held.dogfood.test.ts`
boots the showcase through `bootStack` with a database file, and
cold-boots it once.
**Ablation, run once and not kept.** The five fix sources were reverted
to the merge base on the working tree, and the three packages were
rebuilt. Each marker was proved absent from `dist/` with
`scripts/ablation-dist-preflight.mjs --absent`. The dogfood file then
read **8 failed / 5 passed of 13**. The five that stay green both ways
are the preservation pins: the two locked-base refusals (#20679's), the
two `/meta` round trips, and the other-type control. After the restore,
`git status --porcelain` was empty, the markers were present in `dist/`
again, and the file read **13 / 13**. A first attempt left
`@objectstack/service-automation`'s build red: its barrel still exported
the new type. That meant `@objectstack/runtime`'s `dist/` was never
rebuilt, so the first reading was void. It was redone with the barrel
reverted too.
## Round trips (the triage direction `5903721942`)
- **Studio: NOT MEASURED.** The Studio source is in `objectui`, which is
not in this container. What Studio receives was measured on the showcase
boot: the served document on both read doors.
- A customer flow created through the automation door is served with no
provenance stamps.
- A customer flow stored through `/meta` is served with none either.
- A shipped flow is served with its package's stamps.
- **A customer flow stored bound to a package is served with that
binding surfaced as a package stamp and no tenant marker.** The
canonical classifier alone reads that as code-shipped, so a literal
"stamps classify it as code-shipped ⇒ refuse" rule would have broken
this legitimate round trip (measured: 422 before the edge was fixed).
The rule therefore also asks the server's own fact behind the stamp: the
package the stored row of that name is bound to, or the base the write
itself names. That round trip is pinned on both doors, including across
a cold boot.
- **CLI:** `os meta register` sends a file's contents verbatim to `PUT
/meta/:type/:name` (`packages/cli/src/commands/meta/register.ts:59-76`),
and `os meta get` prints the served document. So a get → register round
trip is the `/meta` round trip above: a customer flow is accepted, a
package-bound customer flow is accepted, and a shipped flow is refused
as a locked base. The CLI adds no stamps of its own.
## Verification (at `f0de8fe69`, after merging `origin/main`
`30839063b`)
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 192
files passed, 3 skipped; 2813 tests passed, 19 skipped.
- `pnpm --filter @objectstack/service-automation exec vitest run`: 158 /
158 files, 1984 / 1984 tests.
- `pnpm --filter @objectstack/runtime exec vitest run`: 298 / 298 files,
4961 passed, 1 skipped.
- dogfood: `flow-provenance-server-held`,
`packaged-flow-write-door-parity`, `automation-flow-clone-door`,
`automation-toggle-tenant-scope` and `packaged-activation-ledger-reach`
passed, 42 / 42.
- `typecheck`: `metadata-protocol`, `service-automation` (with
`check:test-typecheck`), `runtime` and `dogfood`, all exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands`: all 69 derived
families were run, each with its exit code recorded. `--ran` reconciles
69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
- `check-plugin-teardown-shape --self-test` first exited 3 on the
shallow clone. It exited 0 after deepening, as it prescribes.
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET:
eight unrelated packages had no `dist/`. It exited 0 after they were
built.
- **Lint, as a proven narrowing, not a full `pnpm lint`:**
1. Population: every changed TypeScript file sits under `packages/**`,
which the `packages/**/*.{ts,…}` and `**/*.{ts,…}` blocks of
`eslint.config.mjs` lint.
2. `eslint --no-inline-config --format json` over the 23 changed files:
23 files, 0 errors, 0 warnings.
3. Invariance: the config never enables type-aware linting
(`eslint.config.mjs:327-328`), so this diff cannot move a verdict on any
untouched file.
## Acceptance notes
- **The automation create and update doors still persist nothing (not
built here, as the claim allowed).** Measured on the showcase boot with
a database file:
- a flow created through the create door answers `200` and reads `404`
after a cold boot;
- an update through the update door, on a flow stored through `/meta`,
answers `200`, and the stored definition wins after a cold boot.
The clone's save does not cover these doors for free: making them
durable changes both doors' contract. Named in the report; no carrier.
- **A `/meta` flow save naming a package id no package has answers
`200`** on the showcase's host-config topology. Measured, out of scope.
Named in the report.
- **The loader's set is the registry's view, for both the lock and the
engine.** They read one source, so they cannot disagree. A dev-time
artifact reload that adds a code flow is not re-registered into that
view until restart (source reading, not measured). That is the same for
the #20679 lock.
- The engine used to count a flow stored bound to a package as packaged
at boot, because its served definition carries that binding as a package
stamp (see "Round trips"). It now reads the loader's set, so that
misclassification is gone too.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016SuKjJJSq2iLTm4Z2PbAmY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent b531c7b commit 76bd58f
24 files changed
Lines changed: 1399 additions & 70 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- qa/dogfood/test
- runtime/src
- domains
- services/service-automation/src
| 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 | + | |
Lines changed: 5 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
213 | | - | |
| 213 | + | |
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| |||
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
283 | 286 | | |
284 | 287 | | |
285 | 288 | | |
286 | 289 | | |
287 | 290 | | |
288 | 291 | | |
| 292 | + | |
289 | 293 | | |
290 | 294 | | |
291 | 295 | | |
| |||
Lines changed: 201 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 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
0 commit comments