Repository navigation
Commit 4d0b9cd
Fixes #20819
Clause-②: no
## What changes
When a write or a removal targets a flow that a code package ships,
without naming that package with `?package=`, the package doors in
`packages/metadata-protocol` refuse it with `403 NOT_OVERRIDABLE`. (A
write that names the shipping package takes the named-base limb and is
refused `403 ITEM_LOCKED`, unchanged by this PR; corrected by the seat
after review 5916639859.) The refusal's sentence is now chosen **per
ADR-0126 regime**, from one table keyed on the type's declared regime:
- `PACKAGED_BASE_REGIME` (`protocol.ts:14704`) is the regime
declaration. It holds `flow: 'C'` and nothing else.
- `PACKAGED_BASE_REFUSAL_BY_REGIME` (`protocol.ts:14732`) is the
per-regime sentence table. It has one row, Regime C.
- `packagedBaseRegimeSentence` (`protocol.ts:14749`) looks up the table.
The two emitters, `refusePackagedBaseOverride` for save and
`refusePackagedBaseRemoval` for delete, ask it for their sentence and
fall back to their old literal, which is kept byte for byte. There is no
`type === 'flow'` branch in the emitters.
The Regime C sentence, as served by `PUT
/api/v1/meta/flow/pkg_alert_flow`:
```text
Metadata item 'flow/pkg_alert_flow' is provided by a code package, and its packaged base is locked against in-place edits. Clone it under a new name to customize it (POST /api/v1/automation/:name/clone, body {name, label}), or switch it off (POST /api/v1/automation/:name/toggle, body {enabled: false}; operator-only where one install serves several organizations). See docs/adr/0126-packaged-metadata-customization-model.md.
```
On removal, "against in-place edits" reads "against removal". The
sentence names the clone (§7.1) and the switch (§7.2), cites ADR-0126,
and does not mention `OS_METADATA_WRITABLE` or a redeploy.
**What does not move** (Clause-② `no`): the code, the status, and which
writes are refused. The predicate is unchanged, and
`OS_METADATA_WRITABLE=flow` still opens the lock at both doors (the
existing hatch pin stays green; only its comment changed). The
named-base `ITEM_LOCKED` limb is unchanged. Every type with no declared
regime keeps its sentence byte for byte.
## Zone 2 readings (PM hypotheses)
- **H1 held.** `packagedBaseRefusal` is at `protocol.ts:14513` (about
`:14506` at base `33b6e8bec`). Every door reaches the same two emitters,
and every caller relays the sentence unchanged:
- `saveMetaItem` / `deleteMetaItem` call the emitters directly, behind
`environmentId !== undefined`.
- `tenantAuthoredWriteRefusal` returns `packagedBaseRefusal` for a flow
in the loader's set. It is asked by `saveMetaItem` (the `/meta` PUT, on
both topologies) and by runtime `refuseUnauthoredFlowWrite` (`POST
/automation`, `PUT /automation/:name`, and the clone target). The
runtime relays it through `deps.errorFromThrown(refusal)`.
- runtime `refusePackagedFlowBaseChange` covers `DELETE
/automation/:name`, and PUT/POST when the protocol has no authoring
rule. It relays through `deps.errorFromThrown(refusal)`.
- The REST `/meta` routes relay it through `handleRouteError`, verbatim
below 500 characters.
- The runtime pin "ONE emitter" compares the messages across the doors,
and they are equal.
- **H2: no regime is declared in code.** `git grep -i regime` over
`packages/{spec,metadata-protocol,metadata-core,runtime,core,types}/src`
finds prose comments only. `DEFAULT_METADATA_TYPE_REGISTRY` entries
carry no regime field. So the smallest declaration went in beside the
door: a map from type to regime, holding Regime C = `flow`.
- ADR-0126 §3's full Regime C list: `flow`; `permission`; `action` (the
disable half; the clone half is not chartered, §8 item 2); pre-charted
`tool`, `skill` and `position`.
- Two other Regime C types reach this door. `action` reaches it on save
and delete. `permission` reaches it on save only: its delete is exempted
by the `supportsOverlay` carve-out.
- Neither is declared. The Regime C sentence names the `/automation`
routes, which exist for a flow alone. `action`'s switch is `POST
/api/v1/actions/_activation/:object/:action` and it has no clone;
`permission` has its own clone machinery.
- `tool`, `skill` and `position` reach the door only if a package ships
one. The measurement recorded in `isNestedArtifactField`'s comment found
none on the showcase.
- The `action` control pin holds the undeclared sentence byte-identical.
Measured at the REST route, see the Acceptance notes.
- **H3 held.** Both routes are served on `main`: `POST
/automation/:name/toggle` at `automation.ts:2427` and `POST
/automation/:name/clone` at `automation.ts:2513`, under the
`/automation` domain prefix.
- The toggle is gated by `manage_metadata`, then by
`refuseUngrantedActivationWrite`, which requires the platform operator
in the `group` / `isolated` postures (ADR-0126 §5).
- The clone body `{ name, label }` needs both keys. The toggle body is
`{ enabled?: boolean }`.
- The runtime test's "what the lock leaves open" cases drive both routes
to `200` on a packaged flow.
- **H4 applies, and it holds.** `CLIENT_MESSAGE_MAX = 500`
(`packages/rest/src/error-response.ts:72`) truncates the tail at 500
characters.
- The sentence is 411 characters before the name on save and 404 on
removal: 425 and 418 with `pkg_alert_flow`. Any name up to 88 characters
arrives whole.
- The protocol pin asserts a length under 500. The REST pin asserts the
served text still ends with the ADR citation.
## Tests (all at head `d58d5578f6` unless stated)
-
`packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts`,
with 8 new cases:
- flow save and delete, on both topologies: 403 `NOT_OVERRIDABLE`, clone
and toggle named, ADR-0126 cited, no hatch, no redeploy, under 500
characters;
- the plural spelling folds to the same sentence;
- the `/meta` doors throw the regime sentence;
- control: `page` keeps its sentence byte for byte;
- control: `action` keeps its save and delete sentences byte for byte.
Result: `Tests 17 passed (17)`.
- `packages/runtime/src/domains/automation-packaged-base-lock.test.ts`:
`PUT` and `DELETE /automation/:name` (both topologies) and `POST
/automation` onto a packaged name now also assert that the relayed
message names clone and toggle and does not name the hatch. The hatch
test's title and comment are corrected: the refusal no longer names the
hatch, and the hatch still opens the door. `Tests 15 passed (15)`.
- `packages/rest/src/rest-meta-packaged-flow-refusal.test.ts` (new)
drives the real `PUT` / `DELETE /api/v1/meta/:type/:name` routes over a
real protocol and `SchemaRegistry`: flow PUT on both topologies, flow
DELETE on an environment kernel, and a `page` control compared byte for
byte. `Tests 4 passed (4)`.
- Full `@objectstack/metadata-protocol` suite, run at `af8a9ee7a9` (the
merge since then brought no change to this package): `Test Files 193
passed | 3 skipped (196)`, `Tests 2870 passed | 19 skipped (2889)`.
- Typecheck at `af8a9ee7a9`: `pnpm --filter` `typecheck` passes for
`@objectstack/metadata-protocol`, `@objectstack/rest` and
`@objectstack/runtime`. `tsc --listFiles` counts each of the three test
files once in its package's program. `check:test-typecheck` is OK for
rest (0 held) and runtime (the identity-pinned ledger is unchanged).
- The `packages/cli` tiers were not touched. The runtime and rest full
suites are left to CI; this PR changes only the one test file in each.
## Ablation on the regime choice (committed fix first; `node
scripts/ablation-replace.mjs` with its restore trap, `pnpm --filter
@objectstack/metadata-protocol build`, then `node
scripts/ablation-dist-preflight.mjs`; rest and runtime read the `dist/`)
Predicted before running: leg A turns the flow pins red and leaves the
controls green; leg B turns only the `page` controls red. Both came out
as predicted.
| leg | on-disk proof | dist preflight | metadata-protocol | runtime |
rest |
|:--|:--|:--|:--|:--|:--|
| A: delete `flow: 'C',` | anchor 1→0, blob `f1e6267e`→`787e1532` |
marker absent from all 24 built files | 5 failed / 12 passed | 5 failed
/ 10 passed | 3 failed / 1 passed |
| A restore | blob == HEAD `f1e6267e`, `git diff HEAD` empty | marker
present in 2 built files; tree clean | 17 passed | 15 passed | 4 passed
|
| B: declare `page: 'C'` | anchor 1→0, blob `f1e6267e`→`fbb29b3b` |
marker present in 2 built files | 1 failed (`page` control) / 16 passed
| 15 passed | 1 failed (`page` control) / 3 passed |
| B restore | blob == HEAD, `git status --porcelain` empty | marker
absent from all 24; tree clean | 17 passed | 15 passed | 4 passed |
The first leg-B attempt was a no-op: `ablation-replace` refused it
because the replacement contained the anchor ("anchor count moved 1 →
1") and restored the file. It was re-run with a non-overlapping anchor,
and the row above is that run. Both runs were at `af8a9ee7a9`.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands` (no paths), at
`d58d5578f6`, derived 63 commands. The change set is 5 paths, 366
changed lines against the 5000-line human-merge threshold: under. All 63
were run at `d58d5578f6`, and `--ran` reports: `✓ dispatch-gates --ran:
63 derived famil(ies) accounted for — 61 run, 2 NOT-MEASURED (0 DERIVED
from a recorded exit 3, 2 claimed)`.
- 61 exited 0, including `check:doc-authoring` (the new sentence carries
no tracker number), `check:nul-bytes`, `check:engine-double-contract`,
`check:cross-package-test-inputs`, `check:test-source-alias`,
`check:issue-citations` and `check:durability-log-level`.
- NOT MEASURED: `check:dual-build-cjs-loads` and
`check:type-check-debt`, reason: PREREQUISITE NOT MET (exit 3). Both
read the whole workspace's built output. A full packages build is 50
uncached turbo tasks, `@objectstack/console` included (dry run), which
is beyond the shared box's foreground cap. CI's lint.yml builds that
closure first.
- The branch merged `origin/main` (`d58d5578f6`) because the first
derivation warned of a stale tree
(`scripts/pm/check-governed-queue-guard.mjs` had moved). The incoming
commits touch `driver-memory` and that script only. The re-derivation
printed the same 63 commands with no stale warning. The build closure
was refreshed and the three pinned files re-run at `d58d5578f6` (17 / 15
/ 4 passed).
Lint was narrowed to the changed files, not the repo-wide `pnpm lint`
(that run is CI's). Three pieces of evidence make that narrowing
complete:
1. The population comes from eslint's own config: `eslint
--no-inline-config --print-config` resolves a config for each of the 4
changed `.ts` files, so none is ignored.
2. `eslint --no-inline-config --format json` on those files counts
`files 4 errors 0 warnings 0`, at `d58d5578f6`.
3. `eslint.config.mjs` never enables type-aware linting ("no
`parserOptions.project`, no typed `@typescript-eslint` rules", at its
line 328), so this diff cannot move the verdict on any file it did not
touch.
## Acceptance notes
- **Other Regime C types keep the hatch sentence (measured; not filed
here).** ADR-0126 §3 also puts `action` and `permission` in Regime C.
Driving the real REST `/meta` route in-process (real protocol, registry
double, environment kernel) gives these answers:
- `PUT /api/v1/meta/action/pkg_approve` answers `403 NOT_OVERRIDABLE`
with "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE
to grant a runtime escape hatch. See docs/adr/0005-…";
- `PUT /api/v1/meta/permission/pkg_perm` gives the same answer;
- `DELETE` on the action answers the removal sentence, which names no
path at all.
The ruling scoped this card to `flow`. Their own sanctioned paths differ
(see H2), so they need their own table rows. That is left to the seat,
together with #15206, which owns whether the hatch survives for sealed
managed content.
- **The named-base `ITEM_LOCKED` limb**
(`SysMetadataRepository.readOnlyBaseOverrideError`,
`sys-metadata-repository.ts:1745`, shared with the repository door)
still prescribes "Edit the source artifact and redeploy, or set
OS_METADATA_WRITABLE=TYPE" for any type, flow included, and cites
ADR-0010. It is reached by `PUT /api/v1/meta/flow/:name?package=PKG`
when PKG is read-only. Reach NOT MEASURED: the harness's engine double
reports every package as writable, so the probe got this PR's
`NOT_OVERRIDABLE` sentence instead. It is outside this card's file
surface. Carrier: none.
- `orgScopedWriteRefusal` and the code-only refusals also name the
hatch. They answer different conditions (an org-scoped write of any
flow; code-only types), and whether the hatch survives is #15206's
question.
## Deviations
- Commit trailers use the model-free pair from AGENTS.md, not the
model-bearing trailer the harness suggested.
- One merge commit (`d58d5578f6`) from `origin/main`; see Gates.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent def279a commit 4d0b9cd
5 files changed
Lines changed: 357 additions & 9 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- rest/src
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
34 | 37 | | |
35 | 38 | | |
36 | 39 | | |
37 | 40 | | |
| 41 | + | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
| |||
119 | 123 | | |
120 | 124 | | |
121 | 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 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4798 | 4798 | | |
4799 | 4799 | | |
4800 | 4800 | | |
| 4801 | + | |
| 4802 | + | |
| 4803 | + | |
| 4804 | + | |
| 4805 | + | |
| 4806 | + | |
| 4807 | + | |
4801 | 4808 | | |
4802 | 4809 | | |
4803 | 4810 | | |
| |||
14669 | 14676 | | |
14670 | 14677 | | |
14671 | 14678 | | |
| 14679 | + | |
| 14680 | + | |
| 14681 | + | |
| 14682 | + | |
| 14683 | + | |
| 14684 | + | |
| 14685 | + | |
| 14686 | + | |
| 14687 | + | |
| 14688 | + | |
| 14689 | + | |
| 14690 | + | |
| 14691 | + | |
| 14692 | + | |
| 14693 | + | |
| 14694 | + | |
| 14695 | + | |
| 14696 | + | |
| 14697 | + | |
| 14698 | + | |
| 14699 | + | |
| 14700 | + | |
| 14701 | + | |
| 14702 | + | |
| 14703 | + | |
| 14704 | + | |
| 14705 | + | |
| 14706 | + | |
| 14707 | + | |
| 14708 | + | |
| 14709 | + | |
| 14710 | + | |
| 14711 | + | |
| 14712 | + | |
| 14713 | + | |
| 14714 | + | |
| 14715 | + | |
| 14716 | + | |
| 14717 | + | |
| 14718 | + | |
| 14719 | + | |
| 14720 | + | |
| 14721 | + | |
| 14722 | + | |
| 14723 | + | |
| 14724 | + | |
| 14725 | + | |
| 14726 | + | |
| 14727 | + | |
| 14728 | + | |
| 14729 | + | |
| 14730 | + | |
| 14731 | + | |
| 14732 | + | |
| 14733 | + | |
| 14734 | + | |
| 14735 | + | |
| 14736 | + | |
| 14737 | + | |
| 14738 | + | |
| 14739 | + | |
| 14740 | + | |
| 14741 | + | |
| 14742 | + | |
| 14743 | + | |
| 14744 | + | |
| 14745 | + | |
| 14746 | + | |
| 14747 | + | |
| 14748 | + | |
| 14749 | + | |
| 14750 | + | |
| 14751 | + | |
| 14752 | + | |
| 14753 | + | |
| 14754 | + | |
| 14755 | + | |
| 14756 | + | |
| 14757 | + | |
14672 | 14758 | | |
14673 | 14759 | | |
14674 | 14760 | | |
| |||
14682 | 14768 | | |
14683 | 14769 | | |
14684 | 14770 | | |
| 14771 | + | |
| 14772 | + | |
| 14773 | + | |
| 14774 | + | |
14685 | 14775 | | |
14686 | 14776 | | |
14687 | 14777 | | |
| |||
14757 | 14847 | | |
14758 | 14848 | | |
14759 | 14849 | | |
| 14850 | + | |
| 14851 | + | |
14760 | 14852 | | |
14761 | | - | |
| 14853 | + | |
| 14854 | + | |
14762 | 14855 | | |
14763 | 14856 | | |
14764 | | - | |
| 14857 | + | |
14765 | 14858 | | |
14766 | 14859 | | |
14767 | 14860 | | |
| |||
14784 | 14877 | | |
14785 | 14878 | | |
14786 | 14879 | | |
| 14880 | + | |
| 14881 | + | |
| 14882 | + | |
| 14883 | + | |
14787 | 14884 | | |
14788 | 14885 | | |
14789 | 14886 | | |
14790 | 14887 | | |
14791 | 14888 | | |
14792 | 14889 | | |
14793 | 14890 | | |
| 14891 | + | |
14794 | 14892 | | |
14795 | | - | |
| 14893 | + | |
| 14894 | + | |
14796 | 14895 | | |
14797 | | - | |
| 14896 | + | |
14798 | 14897 | | |
14799 | 14898 | | |
14800 | 14899 | | |
| |||
0 commit comments