Commit 4b45afa
Fixes #20679
Clause-②: yes (widening)
The `/automation` definition doors now refuse a packaged flow, with the
same locked-base verdict the metadata door gives. This is the public
checklist item `access-security.packaged-flow-write-door-parity`:
clauses 2 and 3 flip to PASS, and clauses 1 and 4 still pass, measured
on a booted showcase. The reproduction stays withheld as filed. This
body stays at the door level the public item already describes.
## What changed
**`packages/runtime/src/domains/automation.ts`**
- `PUT /:name` and `DELETE /:name` call `refusePackagedFlowBaseChange`
before the engine is called, and relay its refusal.
- Order at each door: `manage_metadata` authoring gate, then the body
envelope check (PUT and POST only), then the lock, then the engine.
- A refused write reads and registers nothing. A refused removal
unregisters nothing.
- **Bounded in-place fix, adopted onto the claim's surface by the
seat:** `POST /` onto a name the engine already holds overwrites that
flow. It now takes the same lock, right after the name check.
- Evidence at `3690c44b`, before the fix, from a throwaway probe (real
protocol over a real `SchemaRegistry`, deleted afterwards): a create
onto a packaged flow's name answered `200`, `registerFlow` was entered
once, and the packaged flow's label was overwritten.
- Pinned now: `403 NOT_OVERRIDABLE`, `registerFlow` never entered, and a
create under a new name still succeeds.
- The helper resolves the `protocol` slot with `resolveServiceOrLoud`,
unscoped, exactly as the `/meta` domain resolves it. So both doors ask
the same protocol instance.
- If the slot is wired but fails to resolve, the error is re-raised: the
write fails and does not proceed as unlocked.
- A composition with no metadata protocol keeps today's behaviour. It
has no `/meta` door to be at parity with.
**`packages/metadata-protocol/src/protocol.ts`**: the widening, a
cross-lane surface the seat adopted.
- New public method
`ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name,
operation })`. It returns the refusal the `/meta` door gives for writing
(`'save'`) or removing (`'delete'`) an existing item that a code package
ships, or `null` when that door would not refuse on this ground.
- The `/meta` verdict (`isArtifactBacked` + `isOverlayAllowed`, and its
emitters) was private to this class, so a second door could not ask it
any other way.
- The verdict is lifted, not copied. Two private helpers,
`refusePackagedBaseOverride` and `refusePackagedBaseRemoval`, carry
`saveMetaItem`'s and `deleteMetaItem`'s inline package-door code
verbatim (proof below).
- Both methods call the helpers where the inline code stood.
- The helpers still throw, as that code did.
- `packagedBaseRefusal` is the one place a throw becomes a value. It
re-raises anything that is not a `403` `NOT_OVERRIDABLE` /
`ITEM_LOCKED`.
- **Why runtime relays the refusal instead of stamping its own code.**
`NOT_OVERRIDABLE` and `ITEM_LOCKED` are ledgered under
`@objectstack/metadata-protocol` (ADR-0112). `@objectstack/runtime`'s
owner key lists neither.
- A runtime stamp would need a ledger row or a waiver in
`packages/spec`.
- It would also be a second emitter for one condition.
- Relayed through `errorFromThrown`, the code, status and sentence are
the producer's.
- `check:error-code-provenance`: 330 stamp sites, 313 listed, 17 waived,
OK.
**What the lock keys on.** The flow's NAME, looked up in the registry's
artifact-only lookup (`SchemaRegistry.getArtifactItem`,
`packages/objectql/src/registry.ts:3919`). That lookup scans the
PACKAGE_ID:NAME entries the artifact loader registers.
- The door hands the verdict `{ type, name, operation }` and nothing
else.
- Neither the request body nor the engine's registered flow is
consulted.
- Pinned both ways: a packaged name is refused whatever provenance
stamps the body carries, and a customer flow is not locked by a body
that claims a package.
**Two refusals on DELETE.** The lock (`403 NOT_OVERRIDABLE`) answers
before the engine's ADR-0126 §7.3 refusal (`DELETE_RESTRICTED` / `409`,
a packaged subflow that packaged callers still reach).
- Every packaged flow is locked first, so at this door the §7.3 refusal
is reached only where the lock admits the removal: with
`OS_METADATA_WRITABLE=flow`.
- The engine's guard is unchanged.
- ⛔ No lock was added to `registerFlow` / `unregisterFlow`: the boot
pull registers packaged flows through them.
**What stays open.**
- A flow no code package ships: created, updated and removed as before.
- `POST /:name/clone`, the ADR-0126 §7.1 customization path.
- `POST /:name/toggle`, the activation switch. Its packaged-only rule
from #20726 is untouched.
- The `OS_METADATA_WRITABLE=flow` operator hatch, which the refusal
sentence names. It opens this door exactly as it opens `/meta`: same
`isOverlayAllowed`.
## Lift proof: each lifted helper body, before and after,
whitespace-insensitive
```bash
git show f284ab2:packages/metadata-protocol/src/protocol.ts | sed -n '16014,16093p' | tee old-save.txt | wc -l # 80: saveMetaItem's inline package door
git show f5ea006:packages/metadata-protocol/src/protocol.ts | sed -n '14300,14379p' | tee new-save.txt | wc -l # 80: refusePackagedBaseOverride body
diff -w old-save.txt new-save.txt | wc -l # 0
git show f284ab2:packages/metadata-protocol/src/protocol.ts | sed -n '21920,21931p' | tee old-delete.txt | wc -l # 12: deleteMetaItem's inline refusal
git show f5ea006:packages/metadata-protocol/src/protocol.ts | sed -n '14401,14412p' | tee new-delete.txt | wc -l # 12: refusePackagedBaseRemoval body
diff -w old-delete.txt new-delete.txt | wc -l # 0
```
| helper | lines before | lines after | `diff -w` changed lines |
|:--|--:|--:|--:|
| `refusePackagedBaseOverride` | 80 | 80 | 0 |
| `refusePackagedBaseRemoval` | 12 | 12 | 0 |
The only added lines compute the locals the inline code read from its
enclosing method:
- `overlayAllowed` in the override helper;
- `overlayAllowed` and `artifactBacked` in the removal helper.
Each is spelled exactly as in the calling method. `deleteMetaItem` keeps
its own copies for its `NOT_CREATABLE` check.
## Pins
- `packages/runtime/src/domains/automation-packaged-base-lock.test.ts`
(15 cases). Real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`, with the packaged flow registered the way the loader
registers it; the automation service is a spy.
- PUT and DELETE refused on a host-config kernel and on an environment
kernel.
- The door's answer equals `saveMetaItem`'s / `deleteMetaItem`'s thrown
refusal: code, status and sentence.
- The POST create-overwrite.
- Body stamps decide nothing.
- The envelope check keeps its place.
- Controls: customer flow, runtime-row and tenant-bound registry items,
clone, toggle, the hatch, no protocol, a failing protocol (not
fail-open).
-
`packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts`
(9 cases):
- equality with the two methods' throws;
- the type folded at the producer;
- `null` for a name no package ships and for a Regime O type;
- the #6960 delete carve-out;
- the hatch;
- a failing lookup re-raised, never handed out as a verdict.
-
`packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts`:
the derived fold population gains `packagedBaseRefusal` ("all
fourteen").
-
`packages/qa/dogfood/test/packaged-flow-write-door-parity.dogfood.test.ts`
(5 cases), the real showcase composition over HTTP:
- Clause 1 control: `PUT /meta/flow/:name` answers `403`, code
`NOT_OVERRIDABLE`, in the REST door's envelope.
- Clauses 2 and 3: `PUT` / `DELETE /automation/:name` answer `403
NOT_OVERRIDABLE`, and the definition reads back byte-identical.
- Clause 4: no residue. The enabled/bound row is unchanged.
- A customer flow is created, updated and removed through the same door.
## Tests (measured)
At `3690c44b` (merge over #20726):
- `@objectstack/runtime` `local` project: 292 files, 4215 passed, 1
skipped.
- `@objectstack/runtime` `repo` project: 727 passed.
- `@objectstack/metadata-protocol`: 191 files passed (3 skipped); 2801
passed, 19 skipped.
- `@objectstack/metadata-protocol` typecheck: exit 0.
- `@objectstack/objectql`, the 20 files pinning `NOT_OVERRIDABLE` /
`NOT_CREATABLE` / `ITEM_LOCKED` on `saveMetaItem` / `deleteMetaItem`:
362 passed.
- `@objectstack/rest`, 3 files: 41 passed.
At `f5ea0060` (head):
- Every `automation-*.test.ts` in `@objectstack/runtime`: 25 files, 478
passed.
- `@objectstack/runtime` typecheck: exit 0, `check:test-typecheck` OK.
- The two protocol pin files: 27 passed.
- Dogfood pin, after a runtime rebuild: 5 passed.
## Reverse verification
Each leg was committed first, then mutated through
`scripts/ablation-replace.mjs` (anchor hit 1, blob changed). Dist legs
were proven by `ablation-dist-preflight` (marker in 2 built files).
Every restore was proven (blob == HEAD, `git diff HEAD` empty, tree
clean, marker absent from dist). Predicted and measured agree on every
leg.
| leg | head | suite | predicted | measured |
|:--|:--|:--|:--|:--|
| door helper disabled (runtime src) | `3690c44b` | runtime pin | 7 red
/ 7 green | 7 red / 7 green |
| same, runtime rebuilt | `3690c44b` | dogfood pin | 3 red / 2 green | 3
red / 2 green |
| `packagedBaseRefusal` returns `null` (rebuilt) | `3690c44b` | protocol
pin | 6 red / 3 green | 6 red / 3 green |
| same | `3690c44b` | runtime pin | 6 red / 8 green | 6 red / 8 green |
| re-raise discriminator widened | `3690c44b` | protocol pin | 1 red / 8
green | 1 red / 8 green |
| POST call removed | `f5ea0060` | runtime pin | 1 red / 14 green | 1
red / 14 green |
After every restore, all pins were green again.
## Gates
- `dispatch-gates --commands` at `f5ea0060`: 67 derived. `--ran`
reconciliation: 67 run, 0 NOT-MEASURED, 0 unrun, every exit 0.
- Also run: `check:error-code-casing` and `@objectstack/spec`
`check:error-code-provenance`, both exit 0.
- `pnpm lint`, narrowed and proven:
1. The population comes from `eslint.config.mjs` (the `**/*.{ts,…}` and
`packages/**/*.{ts,…}` blocks). All 6 changed `.ts` files are in it.
2. `--format json` counted 6 files linted, 0 errors, 0 warnings, at
`f5ea0060`.
3. The config never enables type-aware linting (`eslint.config.mjs`
states it: no `parserOptions.project`, no typed rules), and its four
plugins are local AST rules. So this diff cannot move any untouched
file's verdict.
## Acceptance notes (observed, not filed)
- **The ADR-0126 §2 refusal wording.** §2 says the refusal names the
sanctioned path. For a packaged flow, the shared `NOT_OVERRIDABLE`
sentence names "edit the source artifact and redeploy" and the
`OS_METADATA_WRITABLE` hatch. It does not name clone (§7.1). This holds
on both doors, because the sentence is one emitter. It is reported to
the seat, not changed here.
- **Two envelopes for one refusal.** On this composition `/meta` answers
through the REST server's refusal envelope, `{ error, code }` with a
flat string `error`. `/automation` answers through the dispatcher's, `{
success, error: { code, message } }`. Pre-existing. The dogfood pin
reads each where it lives.
- **`@objectstack/rest` inlines the protocol.** It declares
`@objectstack/metadata-protocol` as a devDependency, so its built
`dist/` carries its own copy of the protocol class. Pre-existing. It is
rebuilt with the same source.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 73155fe commit 4b45afa
7 files changed
Lines changed: 1012 additions & 93 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- qa/dogfood/test
- runtime/src/domains
| 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 | + | |
Lines changed: 121 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 | + | |
Lines changed: 4 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 | | |
| |||
0 commit comments