Repository navigation
Commit 0c5a71b
fix(metadata-protocol): a packaged action or permission set's refusal names its own sanctioned path (ADR-0126 Regime C rows) (#21026)
Fixes #20910
Clause-②: no
## What this changes
ADR-0126 §3 puts `flow`, `action` and `permission` in Regime C, and §2
requires the Regime C refusal to name the sanctioned path. Before this
PR, only `flow`'s package-less refusal at the environment-scoped package
door did that. Every other refusal of an in-place write onto one of
these packaged items prescribed the `OS_METADATA_WRITABLE` hatch or a
redeploy, and a packaged action's removal named no path at all.
This PR follows triage's direction on the card (one regime, per-type
sanctioned routes, the sentence built from the row) and the seat answer
on Q1 and Q2 (comment 5923536089):
- **One table, one new module.**
`packages/metadata-protocol/src/packaged-base-regime.ts` holds
`PACKAGED_BASE_REGIME`. Each row is `{ regime, routes }`, and `routes`
declares only the primitives the type has: `clone`, `switchOff`, or
both. A row that names neither does not compile. The module builds the
prescription (the row's paths in a fixed order, clone then switch, then
the ADR-0126 citation) and the whole package-door sentence. Nothing in
it reads the type.
- The module is not exported from the package entry.
- It lives in its own module because `protocol.ts` imports
`sys-metadata-repository.ts`, so the table could not live in either file
without an import cycle.
- **Rows:**
- `flow`: clone (`POST /api/v1/automation/:name/clone`) and switch
(`POST /api/v1/automation/:name/toggle`).
- `action`: the switch only, `POST
/api/v1/actions/_activation/:object/:action` with `{enabled: false}`,
where `:object` is `global` for an object-less action. No clone, because
the action-clone half is not chartered. Its operator-only clause matches
the flow toggle's, since both routes run the one ADR-0126 §5 gate.
- `permission`: the clone only, spelled as `plugin-security`'s own lock
refusal spells it.
- **Three doors read it. No door has a per-type prose branch.**
1. The protocol's package door (`refusePackagedBaseOverride` /
`refusePackagedBaseRemoval`), on an environment-scoped kernel, answers
`403 NOT_OVERRIDABLE`. `flow`'s sentence is byte-identical, held by a
literal pin.
2. **The repository's type door**
(`SysMetadataRepository.assertAllowed`) is where a write lands when the
`/meta` protocol is not environment-scoped, which includes the default
`pnpm dev` boot. For an override-artifact write with the hatch closed,
on a type with a row, it now answers the same row-built sentence, `403
NOT_OVERRIDABLE`, on save and on removal. Every other type keeps the
overlay-allowed-list sentence and its pins.
3. **The named-base `ITEM_LOCKED` limb** (`readOnlyBaseOverrideError`)
applies with the hatch closed, on a type with a row. Its opener is
shortened to "Cannot overlay 'TYPE' in package 'ID': that package is
read-only, and its packaged base is locked against in-place edits.",
followed by the row's prescription and the ADR-0126 citation.
- Unchanged: the hatch-OPEN remedy, `code`, `status`, `lockSource`,
`packageId` and `docs`.
- Unchanged: NARROW. A package-less hatch write still lands, and that is
pinned.
- **The #8146 hatch-closed pin moved and was not deleted.** It now sits
on `page`, a type with no regime row, and is measured to reach the same
limb (`ITEM_LOCKED`, `lockSource: 'package'`, the named base echoed
back). A Regime C pin sits beside it, and the pin's comment cites
ADR-0126 §2 as the reason the two sentences differ.
- **Docs.** `content/docs/concepts/metadata-lifecycle.mdx` had two
sentences that contradicted the new text, and both are corrected:
- "a create/update aimed at a read-only package … is asked to pick or
create a writable base";
- the `permission` row's "edit the package and re-publish".
- A short "Packaged flows, actions and permission sets (ADR-0126)"
subsection lists each type's sanctioned path.
No code, status, route or refused write moves.
**The `Clause-②: no` check** (the seat answer's stop (a)).
`assertAllowed` / `assertDeleteAllowed` now take the ref and the
operation, and both are `private`. `readOnlyBaseOverrideError`'s
signature is unchanged. Measured on the built declarations, with
comments stripped, before against after this round (`dist/index.d.ts`
and `dist/index.d.cts`): the only lines that differ are the removal of
three `private static` member names on
`ObjectStackProtocolImplementation` (`PACKAGED_BASE_REGIME`,
`PACKAGED_BASE_REFUSAL_BY_REGIME`, `packagedBaseRegimeSentence`), which
the table move took out of the class. No exported signature moved, and
the new module appears nowhere in the declarations. **The seat answer's
stop (b)**: no type without a regime row changes its sentence at any
door. The controls are `page` and `object` here, and `page` on the
booted app below.
**Lengths** against the REST door's 500-character bound
(`truncateClientMessage` keeps a message only while it is shorter than
500):
| Refusal | Row | Characters before the variable part | Longest variable
part that arrives whole | Pin |
|:--|:--|:--|:--|:--|
| Package door / type door (save / removal) | `flow` | 411 / 404 before
the name | 88-character name | yes |
| Package door / type door (save / removal) | `action` | 365 / 358
before the name | 88-character name | yes |
| Package door / type door (save) | `permission` | 317 before the name |
88-character name | yes |
| `ITEM_LOCKED` named base | `flow` | 422 before the package id |
77-character package id | 64-character id |
| `ITEM_LOCKED` named base | `action` | 376 before the package id |
123-character package id | 64-character id |
| `ITEM_LOCKED` named base | `permission` | 328 before the package id |
171-character package id | 64-character id |
The longest package id this repository ships is
`com.objectstack.platform-objects.activation-ledger`, at 50 characters.
## Measured
**Booted showcase** (`pnpm dev -- --fresh`, local mode, discovery
`scoping.enabled: false`). Before is the previous head of this PR,
`d9b1a8e8b`, byte-identical to `origin/main` `7fa67dada`. After is
`9c169378a`.
| request | before | after |
|:--|:--|:--|
| `PUT
/api/v1/meta/flow/showcase_task_completed?package=com.example.showcase`
| `403 ITEM_LOCKED` "…locked against runtime edits. Edit the source
artifact and redeploy, or set OS_METADATA_WRITABLE=flow to grant a
runtime escape hatch on this TYPE (…pair it with a package-less write).
See docs/adr/0010-…" | `403 ITEM_LOCKED` "Cannot overlay 'flow' in
package 'com.example.showcase': that package is read-only, 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." |
| `PUT /api/v1/meta/action/showcase_mark_done` (type door) | `403
NOT_OVERRIDABLE` "'action' is not allowOrgOverride in the registry.
Overlay-allowed: view, dashboard, report, translation, email_template.
Set OS_METADATA_WRITABLE to enable additional types at runtime." | `403
NOT_OVERRIDABLE` "Metadata item 'action/showcase_mark_done' is provided
by a code package, and its packaged base is locked against in-place
edits. Switch it off (POST /api/v1/actions/_activation/:object/:action,
body {enabled: false}, :object = global for an object-less action;
operator-only where one install serves several organizations). See
docs/adr/0126-…" |
| `PUT
/api/v1/meta/action/showcase_mark_done?package=com.example.showcase` |
`403 ITEM_LOCKED` "…Edit the source artifact and redeploy, or set
OS_METADATA_WRITABLE=action …" | `403 ITEM_LOCKED` "Cannot overlay
'action' in package 'com.example.showcase': that package is read-only,
and its packaged base is locked against in-place edits. Switch it off
(…). See docs/adr/0126-…" |
| `PUT /api/v1/meta/flow/showcase_task_completed` (package-less) |
Regime C flow sentence | identical |
| `PUT /api/v1/meta/permission/showcase_contributor`, with and without
`?package=` | `plugin-security`'s authoring gate, already naming the
clone | identical (that gate answers first on this boot) |
| `DELETE` of the packaged flow, action and permission set | `200`, "No
customization overlay found … already at artifact default" | identical |
| control: `PUT /api/v1/meta/page/showcase_start_here`, with and without
`?package=` | — | still the list-and-hatch sentence and the hatch-closed
`ITEM_LOCKED` remedy "…or set OS_METADATA_WRITABLE=page …", which is
unchanged |
Of the nine readings, exactly the three rows above with a changed
"after" column moved.
**In process, on an environment kernel** (`env_1`), the real `/meta`
route handler over a real `ObjectStackProtocolImplementation` and
`SchemaRegistry`, with the package booted as code. These are the earlier
rounds' readings, still pinned:
- `PUT action` and `DELETE action` name the switch.
- `PUT permission` names the clone.
- `DELETE permission` is the `supportsOverlay` carve-out, not refused.
- `?package=` on both kernel shapes answers `ITEM_LOCKED` with the row's
path.
**Docs drift** (`content/docs/concepts/metadata-lifecycle.mdx`, matched
by `ObjectStackProtocolImplementation`). Two sentences contradicted the
new text and are corrected, as listed under "What this changes". The
page names no `OS_METADATA_WRITABLE` prescription for a flow, action or
permission edit. The docs-drift-check comment (5923480340) also lists
four release-owned pages. They were read and none of them states this
refusal; they are release-owned and were not touched.
## Tests
All at `9c169378a`.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 195 files passed and 3 skipped; 2920 tests passed and
19 skipped.
- `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. The
tsconfig includes `src/**/*`, tests included.
- `pnpm --filter @objectstack/rest typecheck`: exit 0, with
`check:test-typecheck` OK.
- REST pins (`rest-meta-packaged-action-permission-refusal.test.ts` and
`rest-meta-packaged-flow-refusal.test.ts`): 2 files, 15 tests passed.
- New and moved pins:
- `sys-metadata-repository.package-writability.test.ts`:
- the type door on save, for each row, with the exact sentence;
- the type door on removal, for `flow` and `action`;
- the type-door control on `object`, byte for byte;
- NARROW, where a hatch write of a packaged action still lands
package-less;
- the named base with the hatch closed, for each row, with the exact
sentence and the full envelope;
- the named base with the hatch open, on `flow`, byte for byte as
before;
- the 64-character package-id bound;
- the moved #8146 pin on `page`, and its Regime C neighbour.
- `protocol.packaged-base-refusal.test.ts`: the named-base limb through
`packagedBaseRefusal` on both kernel shapes. The earlier per-row,
literal-`flow` and 88-character pins stand.
- `protocol.delete-rewrap-envelope.test.ts`: the control-plane removal
prose is partitioned by regime row. `flow` and `action` now read their
removal sentence, and every other type reads the type-door sentence as
before.
- `rest-meta-packaged-action-permission-refusal.test.ts`: a host-config
kernel's `PUT action` and `PUT permission` hit the type door and name
the path. `?package=` for both types on both kernel shapes answers
`ITEM_LOCKED` with the path.
- **Ablation**, run after committing `9c169378a`, through
`scripts/ablation-replace.mjs`. It deleted every row from the table
(anchor 1 → 0, blob `7f06cc1d9f9a` → `b876c9500402`), rebuilt
`@objectstack/metadata-protocol`, and `ablation-dist-preflight --absent`
reported the action-route marker absent from all 24 built files.
- metadata-protocol pins: **24 failed** / 81 passed. Every regime pin
went red, on all three doors, including flow's literal pin and both
length bounds. These stayed green: the moved #8146 pin on `page`, the
`object` type-door control, the hatch-open remedy, NARROW, the `page`
controls and the delete-rewrap file (it partitions by the table, by
design).
- REST pins: **13 failed** / 2 passed. Only the permission `DELETE`
carve-out and the `page` control stayed green.
- Restore leg: blob equals HEAD and `git diff HEAD` is empty; the
rebuild exited 0; the marker is present in 2 built files; the working
tree is clean.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `9c169378a` (no paths, the change set
from merge base `2f2fa11d7`, 9 paths) derives **92** commands. That is
the earlier 62 plus the documentation families the
`content/docs/concepts/` touch adds. All 92 were run there and **every
one exited 0**.
- Reconciled with `--ran` against a `command :: exit N` record: "92
derived, 92 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.
- `origin/main` had not moved past the merge base when the gates were
derived.
- The artifact-roster, wide-population, path-scheduled-job and
type-check-lane families are outside the derived list and are left to
CI.
- `pnpm lint` is CI's run and was not run here.
## Acceptance notes
- The permission row's wording is a copy of `plugin-security`'s
(`packages/plugins/plugin-security/src/packaged-permission-set-lock.ts`),
because `metadata-protocol` cannot import that package. Nothing pins the
two spellings equal across packages. Noted, not filed.
Body refreshed for patch round 1 at 2026-10-01T03:16Z.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5dbeb7d commit 0c5a71b
9 files changed
Lines changed: 839 additions & 114 deletions
File tree
- .changeset
- content/docs/concepts
- packages
- metadata-protocol/src
- rest/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 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
95 | | - | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
96 | 98 | | |
97 | 99 | | |
98 | 100 | | |
| |||
109 | 111 | | |
110 | 112 | | |
111 | 113 | | |
112 | | - | |
| 114 | + | |
113 | 115 | | |
114 | 116 | | |
115 | 117 | | |
| |||
126 | 128 | | |
127 | 129 | | |
128 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
129 | 143 | | |
130 | 144 | | |
131 | 145 | | |
| |||
| 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 | + | |
0 commit comments