Repository navigation
Commit ad14957
Fixes #22591
Clause-②: yes
The sealed-item refusal (`403 NOT_OVERRIDABLE`) now names the remedy
that fits what the caller did. This closes out the family that #20819
(flow) and #20910 (action, permission) worked through one type at a
time.
- **An edit** of the item under a held name keeps its sentence, byte for
byte. That is the row's sanctioned path, or "Edit the source artifact
and redeploy" for a type without a row.
- **A create** under a name that a managed package or a built-in holds,
or **a rename into** such a name, is now told: the name is taken; to
author your own item, choose a name no package or built-in holds, and
`GET /api/v1/meta/TYPE` lists the names in use.
Which writes are refused does not change, and neither do the code or the
status.
## Measured at the real doors (showcase, `single`)
The probe was a local file, never committed and deleted after the run.
It booted the real showcase composition through `bootShowcase`, signed
in as the seeded admin, and used the HTTP doors. **Before** was
`origin/main` `d748ae80af`, with the dogfood dependency closure built
from it. **After** was this branch at `6499e49c0f`, with
`metadata-protocol` and `plugin-security` rebuilt from that head.
| Door and request | Before | After |
|---|---|---|
| Setup `POST /data/sys_position` with `manager` (package
`com.example.showcase`) | 403 `NOT_OVERRIDABLE`, "… Edit the source
artifact and redeploy." | 403 `NOT_OVERRIDABLE`, "… so its name is
taken. To author your own position, choose a name no package or built-in
holds (GET /api/v1/meta/position lists the names in use)." |
| Setup create of `guest` (built-in, `com.objectstack.plugin-security`)
| 403, edit remedy | 403, create remedy |
| Setup `PATCH` renaming an admin position into `exec` | 403, edit
remedy | 403, create remedy |
| `PUT /meta/position/manager` with `If-None-Match: *` (the first-write
precondition) | 403, edit remedy | 403, create remedy |
| `PUT /meta/flow/showcase_task_completed` with `If-None-Match: *` |
403, clone and switch | 403, create remedy, citing ADR-0126 |
| `PUT /meta/position/manager` with no precondition, and with
`?mode=draft` | 403, edit remedy | 403, edit remedy (unchanged) |
| `PUT /meta/flow/showcase_task_completed` with no precondition | 403,
clone and switch | unchanged |
| `POST /automation` onto `showcase_task_completed` | 403, clone and
switch | unchanged (see the Acceptance notes) |
| Setup create of `p22591_lead` (control) | 201 | 201 |
`GET /api/v1/meta/position` listed `manager`, `exec`, `everyone`,
`guest` and the other held names, so the remedy's pointer answers.
## One create-aware sentence, not a `position` row
This was the claimant's choice to make by measurement. The create remedy
does not depend on the type: on every type, a create under a held name
has one remedy, which is another name. So `heldNameSentence` in
`packaged-base-regime.ts` is one sentence for every type. Only its
opener and its citation come from the type's row, as in the edit
sentences. An origin-gated `datasource` is "code-defined", not shipped
by a managed package. A Regime C type cites ADR-0126, whose §2 makes a
new machine name mandatory for an item authored beside a packaged one. A
per-type `position` row would have fixed one of the 24 reachable types
and left the rest for a fourth card. A type added to the registry later
gets the create remedy without a row.
## Where the act comes from
The act is the caller's fact and is never inferred. A door that cannot
tell keeps the edit remedy.
- `managedItemSealedSentence(type, name, operation)` takes `'save' |
'create' | 'delete'`. A `'create'` reads `heldNameSentence`.
- `ObjectStackProtocolImplementation.packagedBaseRefusal` takes
`operation: 'create'`, with the same predicate, code and status as
`'save'`. `refusePackagedBaseOverride` passes the act through to the
sentence and to nothing else.
- **Setup** (`plugin-security`, `position-write-through.ts`): the S10
verdict is asked only for a create or a rename into a name, so it now
asks with `operation: 'create'`. This is the producer of the card's
measured symptom, in another package. The dispatch allowed fixing it at
the producer.
- **The `/meta` save door** (`saveMetaItem`): its one fact about the
name is the first-write pin. A present `null` `parentVersion`
(`If-None-Match: *`) asserts that the caller holds no row there, which
the spec calls the first-write pin, so it is read as a create. It
reaches both locked-base doors: `tenantAuthoredWriteRefusal` (flows,
which gains an optional `operation`) and the package door. Any other
save is an edit.
- **`SysMetadataRepository.assertAllowed`** (the third caller the
dispatch named) is unchanged. Its saves are server replays of the item's
own stored history (draft promotion, restore, revert), which are edits,
and it has no create fact to pass.
## Tests
- `packages/metadata-protocol/src/protocol.sealed-remedy-kind.test.ts`
(new):
- **Enumeration pin.** Every type the refusal reaches is measured by
asking the verdict, never from a transcribed list. That is the 23
registry types with no overlay channel plus a plugin-registered type, on
both kernel shapes. Asserted equal to the registry's own flag, with a
floor. For each type, an edit names the edit remedy and a create names
the create remedy. The kind is read off what the sentence points at: the
row's own routes or source (read from `PACKAGED_BASE_REGIME`), the
source/redeploy remedy, or the type's listing route. The wording is not
asserted.
- **Controls.** A create is refused exactly where a save is, with
`NOT_OVERRIDABLE` / 403 on every row. A free name is `null` for both
acts. A removal names no new name. Every create sentence for an
88-character name arrives whole under the REST door's 500-character
bound, with no hatch, clone or switch.
- **The `/meta` door.** For `position` and `flow`: a first-write save
names the create remedy, and an unpinned or `If-Match` save keeps the
edit remedy, with the same code and status. The door's message equals
the verdict's (one emitter).
- `packages/plugins/plugin-security/src/position-write-through.test.ts`:
- S10's "the door's own refusal" pin now compares the Setup create with
the door's first-write save, because a create relays the door's answer
to a create.
- New: a create under a package-held name and under a built-in
(`everyone`) names the create remedy, while the door's edit refusal of
the same name keeps the edit remedy. A rename into `guest` names the
create remedy.
**Ablation, run at `6499e49c0f` with the fix committed first.** Through
`scripts/ablation-replace.mjs`, the line `if (operation === 'create')
return heldNameSentence(singular, name);` was replaced with a comment,
which collapses a create back to the edit remedy. On disk the anchor
count went 1 → 0 and the marker count 0 → 1. Both suites then ran.
`metadata-protocol` had 5 failed and 5 passed: every create row flipped
to `"create": "edit"`, while the reachability, verdict and removal
controls stayed green. `plugin-security` had 3 failed and 41 passed. The
restore put the blob back equal to `HEAD` (`a0fda00025d8`) with `git
diff HEAD` empty. Neither suite reads `dist/`: the test imports
`./protocol.js`, and plugin-security's vitest config aliases
`@objectstack/metadata-protocol` to its `src`. So the ablation needed no
rebuild.
**Runs at `3c3e4ef362`** (this branch after merging `origin/main`
`18d999031b`; the merge brought no change to either touched file):
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: `Test
Files 224 passed | 3 skipped (227)`, `Tests 28314 passed | 19 skipped
(28333)`.
- `pnpm --filter @objectstack/plugin-security exec vitest run`: `Test
Files 194 passed (194)`, `Tests 4096 passed | 45 skipped (4141)`.
- `typecheck` passes for both packages, plugin-security's
`check:test-typecheck` included. `tsc --listFiles` counts each touched
test file once in its package's program.
- The 69 gate commands `dispatch-gates --commands` derives for this diff
all exit 0, reconciled with `--ran`: 69 run, 0 NOT MEASURED. One note:
`check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET` (exit
3) because 8 unrelated packages had no `dist/`. After building them it
exited 0.
- eslint, narrowed to the 5 touched TypeScript files, with
`--no-inline-config` and `--format json`: 5 files, 0 errors, 0 warnings.
Each file is inside the config (`--print-config` resolves). The config
enables no type-aware linting (no `parserOptions.project`), so this diff
cannot move the verdict on any untouched file. The repository-wide `pnpm
lint` is CI's.
## Acceptance notes
- **Studio's create form still gets the edit remedy until objectui
declares its create.** The `/meta` PUT is an upsert, so an unpinned save
onto a held name is an edit by the door's contract. Studio's create mode
sends no precondition (objectui `ResourceEditPage` create path). Its
comment says the door cannot express "expect no row", which has not been
true since the door took `If-None-Match: *`. The server half is here.
The client half is to send `If-None-Match: *` on create. It is reported
for the seat to file in objectui, with this PR as its blocker.
- **`POST /automation` onto a packaged flow's name is unchanged.** That
door's own contract makes a create onto an existing name an overwrite,
which is an edit, so its clone-and-switch remedy matches what it does.
`POST /automation/:name/clone` refuses any target name the engine
already holds with its own name-taken refusal first (read in
`automation.ts`, not probed).
- **Code-only types** (`allowRuntimeCreate: false`) are refused at
`/meta` by their own emitter (`codeOnlyOverrideError`) before the
package door. It says "declare it in source". That is right for a create
too, because such a type has no runtime create.
- **Clause-② reads `yes`.** `packagedBaseRefusal` accepts `operation:
'create'`, and `tenantAuthoredWriteRefusal` takes an optional operation.
That widens a public input surface, so both packages take `minor`. No
verdict, code or status moves. The seat answered the question A and
amended its claim (6093560675). Patch round 1 (6094616222, head
`caeb7b58f1`) changed the changeset only.
- No other region of `protocol.ts` or `sys-metadata-repository.ts` is
touched. The seat's #15206 S5 read-path work does not overlap.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e8c6666 commit ad14957
6 files changed
Lines changed: 388 additions & 16 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- plugins/plugin-security/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 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
112 | 131 | | |
113 | 132 | | |
114 | 133 | | |
| |||
221 | 240 | | |
222 | 241 | | |
223 | 242 | | |
224 | | - | |
| 243 | + | |
225 | 244 | | |
226 | 245 | | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
227 | 249 | | |
228 | 250 | | |
229 | 251 | | |
| |||
307 | 329 | | |
308 | 330 | | |
309 | 331 | | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
310 | 375 | | |
311 | 376 | | |
312 | 377 | | |
| |||
316 | 381 | | |
317 | 382 | | |
318 | 383 | | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
319 | 388 | | |
320 | 389 | | |
321 | 390 | | |
| |||
328 | 397 | | |
329 | 398 | | |
330 | 399 | | |
331 | | - | |
| 400 | + | |
332 | 401 | | |
| 402 | + | |
333 | 403 | | |
334 | 404 | | |
335 | 405 | | |
| |||
0 commit comments