Repository navigation
Commit eda6f14
feat(spec)!: an install through POST /api/v1/packages declares manifest.scope project only, so an authored install cannot lock itself (#24160)
Fixes #24062
Clause-②: no (narrowing)
An install through `POST /api/v1/packages` may now declare
`manifest.scope` `project` only, or leave it out. `cloud` and `system`
are refused at parse time with `400` / `VALIDATION_ERROR`, on both body
forms and on either overwrite spelling, and the refusal says what to
write instead. So an authored install can no longer lock itself.
`ManifestSchema` is unchanged for every other producer.
Session: `session_015p3xNTnaiNHYkAHoQ4y5Cg` (the `os-dev` cloud run
dispatched by the `domain:spec#2` seat, claim `6230547268`). Base
`9f024d8a2c`, head `b78d15a5`.
## What changed
- `packages/spec/src/api/package-api.zod.ts`
- New export `PackageInstallManifestSchema` (plus the types
`PackageInstallManifest` and `PackageInstallManifestParsed`). It is
`ManifestSchema.extend({ scope })` with `scope: z.enum(['project'], {
error }).default('project')`. Every other key, the strict close, its
unknown-key prescription and the retired-key tombstones ride the
`.extend()` unchanged; the authorable-surface shard lists the same key
set, tombstones included.
- New export `packageInstallScopeRefusal(input)`: the one refusal
sentence. It names the value sent, why `cloud` / `system` lock the
package, and the remedy: "Remove `scope` (it defaults to `project`), or
write `scope: 'project'`."
- `PackageInstallRequestSchema.manifest` and the bare branch of
`PackageInstallBodySchema` both now use `PackageInstallManifestSchema`.
The docblocks that named `ManifestSchema` for those two positions now
name the new schema.
- The `overwrite` describe gains: "an id the deployment ships read-only
(code-booted, or scope cloud / system) is refused 422
WRITABLE_PACKAGE_REQUIRED". I re-read PR #24055's door on `main`:
`requireWritablePackage` runs after the 409, for both overwrite
spellings, at `packages.ts` (the `readOnly` leg).
- `packages/runtime/src/domains/packages.ts`: **one line plus its
comment and import, a deviation from the dispatch's "no runtime source
change" expectation, measured and reported below.** `installBodyRefusal`
re-parses the bare form through `PackageInstallManifestSchema`, the
union's own bare branch, instead of `ManifestSchema`.
- Tests: spec pins in `package-api.test.ts`, and the door pins as §9 of
`packages-install-body-contract.test.ts`.
- Generated outputs, regenerated by the generators only: the `api`
shards of `api-surface`, `authorable-surface`, `authorable-defaults`,
`json-schema.manifest`, `declaration-map` and `export-origins`;
`content/docs/references/api/package-api.mdx` and
`references/index.mdx`. Plus one hand-edited ledger,
`dropped-refinements.baseline.json`: the build printed the entry for the
new published schema `api/PackageInstallManifest` (the same
`navigationContributions` site `api/PackageInstallRequest` already
declares), and the header totals 235 to 236 and 766 to 767, which
`scripts/dropped-refinements.test.ts` asserts. Delta vs `main`: 3
exports added, 0 removed.
- `.changeset/24062-authored-install-scope-project.md`:
`@objectstack/spec` `minor` (BREAKING narrowing, launch-window
convention, PR #24055's `62f959dd62` precedent) and
`@objectstack/runtime` `patch`. It carries FROM / TO, the `Clause-②`
line, and the ADR-0087 disposition `not-required
(no-migration-prescription)`. The only conversion that could exist
(rewrite `cloud` / `system` to `project`) is the silent rewrite this
card refuses.
## Accept set, before and after
| Body | Before | After |
|---|---|---|
| wrapped `{ manifest }`, `scope: 'cloud'` or `'system'` | `201`,
authored row stored with that scope | `400` `VALIDATION_ERROR`, nothing
installed; issue `invalid_value` at `manifest.scope` |
| bare manifest, `scope: 'cloud'` or `'system'` | `201` | `400`
`VALIDATION_ERROR`, nothing installed; issue at `scope` |
| either form with `overwrite: true` or `?overwrite=true`, platform
scope | `201` on a fresh id; over an authored project row it re-locked
that row | `400`, ahead of the 409 and the read-only gate; an existing
row keeps its manifest |
| `scope: 'project'`, either form | `201` | `201` |
| no `scope`, either form | `201`, stored as sent (no `scope` key) |
`201`, stored as sent: the door still stores what it was sent, so
absence stays absent |
| `ManifestSchema.safeParse` with `cloud` / `system` / `project` |
accepted | accepted, unchanged |
## Measured: the door needed one line, and why I made it rather than
stopping
A2 assumed narrowing the declaration makes the door refuse with no
runtime change. That holds for the **verdict** and fails for the
**sentence**. Probe on the built `dist`, before the runtime line:
```text
cloud wrapped false bare false
union bare issues: [["invalid_union",[],"Invalid input"]]
ManifestSchema re-parse: true | narrowed re-parse: manifest.scope "cloud" is not a scope a package installed through POST /api/v1/packages ma…
wrapped re-parse: [["invalid_value",["manifest","scope"]]]
```
zod reports the failed union as one `invalid_union`, and
`installBodyRefusal` locates the failure by re-parsing the form the
caller wrote. For the bare form it re-parsed with `ManifestSchema`,
which admits `cloud`. That re-parse passed, so the door fell back to the
union's issues, and the bare-form refusal would have read `(body):
Invalid input`: a `400` that does not say what to write. The door's own
header says the re-read schemas are "the declarations
`@objectstack/spec` publishes for this door; nothing here restates
them", and `ManifestSchema` is no longer that branch. So the fix
re-reads the branch the union actually carries. It adds no door-side
verdict: which bodies are refused is still the declaration's decision
alone. The ablation below shows the line is load-bearing. If the seat
would rather hold the runtime file out, the alternative is the bare-form
refusal without a prescription.
## Producers measured (A3)
`git grep` on `9f024d8a2c` for every sender of `POST /api/v1/packages`
and every parser of `PackageInstallRequestSchema` /
`PackageInstallBodySchema` outside `packages/spec`:
- `@objectstack/client` `packages.install(manifest, options)` forwards
the caller's manifest, and sets no `scope` of its own.
- `packages/runtime/src/domains/packages.ts` is the door itself.
- `packages/client/src/readme-package-install-example.test.ts` and
`packages/runtime/src/domains/packages-install-body-contract.test.ts`
are the two test parsers. Neither fixture carries a platform scope; both
stay green unchanged.
- The CLI (`packages/cli/src`): no sender of this door.
- `metadata-protocol` `duplicatePackage` calls `installPackage`
in-process, not over this door, and **strips** `scope` from the copy
already ("the copy is scope-less"): the same rule this card declares.
- The platform's `scope: 'system'` / `'cloud'` manifests (plugin-auth,
plugin-security, organizations, the service plugins, the apps) reach the
registry by code boot (`registerApp` / `registry.installPackage(…,
'code')`), never through these schemas.
- **Studio (`../objectui`): NOT MEASURED.** The sibling checkout is not
in this container and `packages/console/dist` is absent. Studio creates
bases through this door. If it ever sent a platform scope, the new `400`
names the fix.
No first-party producer sends `cloud` / `system` through these schemas,
so the narrowing breaks nothing first-party.
## Fixtures triaged (A9)
None needed re-spelling. Every test that installs a `scope: 'system'` /
`'cloud'` package seeds the registry directly and never passes through
the door or the narrowed schemas: `packages-readonly-gate`,
`packages-writable-verdict`, `packages-serializable-response`,
`packages-install-overwrite-readonly` (its authored cloud-scoped row is
exactly the already-locked shape, still meaningful), and
`metadata-protocol`'s `get-meta-items-package-writable`. §8's existing
`scope: 'tenant'` row still answers `400` naming `manifest.scope`.
## Rows that already locked themselves (A7): a documented operator
delete, no code remedy
- **Measured:** `service-package` `start()` re-installs every
`sys_packages` row as `authored` with its stored manifest
(`packages/services/service-package/src/index.ts`, the hydration loop's
`registry.installPackage(…, 'authored')`). `isWritablePackage` reads
`manifest.scope` off the registry row. So a row stored with `cloud` /
`system` before this PR is still read-only after it and after every
restart. `DELETE`, `PATCH …/disable` and the overwrite door keep
refusing it `422`; this PR changes nothing for such a row.
- **Recommended remedy: the documented operator delete**, written into
the changeset: `DELETE FROM sys_packages WHERE id = 'PACKAGE_ID'` on the
environment's database, then restart. No API door can do it, by ADR-0070
D2's own design: there is no `isSystem` escape hatch.
- **Why not a startup re-scope:** it is a stored-row rewrite at boot,
the silent-rewrite class the card's filing and its review both advise
against. It would also be a code remedy outside this card's file surface
(a stop-and-report). And no evidence of a real affected row exists: the
defect was found by a deleted probe, and no first-party producer sends
the scope. If the seat or the maintainer wants a code remedy, it is a
separate card on `service-package` hydration.
- Not measured: what happens to `sys_metadata` items such a package
owned once its row is deleted.
## Tests and evidence (head `b78d15a5`)
- Spec build: `VERDICT command-exit 0`.
- Spec vitest `--project local --maxWorkers=2`, full: 690/692 files on
the first run. The 2 red were generated-ledger freshness
(`dropped-refinements` totals; `export-origins` missing the two new
exports), both fixed by regeneration; then
`scripts/dropped-refinements.test.ts` +
`scripts/export-origins.test.ts`: 51/51 passed.
`src/api/package-api.test.ts` + `package-install-one-authority.test.ts`:
105/105.
- `check:generated`: "All 14 generated artifacts are up to date".
- Spec typecheck and runtime typecheck: `VERDICT command-exit 0`.
- Runtime, all 28 `src/domains/packages*.test.ts`: 28 files, 559 tests
passed.
- Client `readme-package-install-example`, `client.test`,
`return-type-precision`: 3 files, 220 tests passed.
- **Ablation 1 (runtime line)**, via `scripts/ablation-replace.mjs`: the
bare-branch re-parse was put back to `ManifestSchema`. Result: exactly
the 4 BARE §9 cases red ("4 failed / 40 passed"), all WRAPPED cases
green. Restore: blob equals HEAD `3168f2e89ab3`, `git diff HEAD` empty.
- **Ablation 2 (spec narrowing, reverse verification through `dist`)**:
the enum was widened back to `cloud` / `system` / `project` with a
marker value. `ablation-dist-preflight`: "marker present in 10 built
files". Runtime contract: 11 failed / 33 passed (every §9 refusal, both
forms, both overwrite spellings, the existing-row overwrite); spec: 2
failed / 90 passed. Restored, rebuilt, `--absent` preflight exit 0,
"working tree clean against HEAD".
- `check-adr-0087-registration`: one declared-breaking changeset,
`[BREAKING+bang+clause-②-narrowing] not-required
(no-migration-prescription)`, exit 0. `check-changeset-no-major`: "This
diff introduces no `major` bump", exit 0.
- `node scripts/pm/check-governed-merges.mjs --branch
claude/issue-24062-authored-install-scope`: "0 of 14 path(s) hit the
register … NOT governed"; 412 changed lines. No `skills/**` path moved.
- Derived gate families (`dispatch-gates --commands`, 110): all run; 108
exit 0. Two exit 3, PREREQUISITE NOT MET, read as NOT MEASURED and left
to CI: `check:skill-examples` (`packages/client-react/dist` not built
here) and `check:dual-build-cjs-loads` (dists outside this closure).
`--ran` reconciliation: "110 derived famil(ies) accounted for — 108 run,
2 NOT-MEASURED (2 DERIVED from a recorded exit 3)".
`check-plugin-teardown-shape --self-test` first refused on the shallow
clone; after fetching its pinned fixture commit: "48 cases pass".
- `main` moved 3 commits past the base (`135e19e20`, `1fee13d4c`,
`c40506aac`). None touches a path in this diff, so I did not merge them;
CI judges the merge ref.
## Acceptance notes
- #24086 (`domain:engine`) is not addressed here. If it makes the
protocol's `installPackage` parse what the door parses, it inherits this
narrowing only by parsing `PackageInstallManifestSchema` (or the body
union), not `ManifestSchema`. Noted for that card, not filed.
- The refusal sentence is pinned only on its subjects (`scope`, the
value sent, `scope: 'project'`) beside `code` and `status`, never in
full.
---
_Generated by [Claude
Code](https://claude.ai/code/session_015p3xNTnaiNHYkAHoQ4y5Cg)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2ac0616 commit eda6f14
14 files changed
Lines changed: 384 additions & 28 deletions
File tree
- .changeset
- content/docs/references
- api
- packages
- runtime/src/domains
- spec
- api-surface
- authorable-defaults
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src/api
| 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 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
| 66 | + | |
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
526 | 526 | | |
527 | 527 | | |
528 | 528 | | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
129 | | - | |
| 128 | + | |
| 129 | + | |
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
133 | | - | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
134 | 138 | | |
135 | 139 | | |
136 | 140 | | |
| |||
860 | 864 | | |
861 | 865 | | |
862 | 866 | | |
863 | | - | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
864 | 871 | | |
865 | 872 | | |
866 | 873 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
748 | 748 | | |
749 | 749 | | |
750 | 750 | | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
751 | 754 | | |
752 | 755 | | |
753 | 756 | | |
| |||
1102 | 1105 | | |
1103 | 1106 | | |
1104 | 1107 | | |
| 1108 | + | |
1105 | 1109 | | |
1106 | 1110 | | |
1107 | 1111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
| 127 | + | |
126 | 128 | | |
127 | 129 | | |
128 | 130 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1255 | 1255 | | |
1256 | 1256 | | |
1257 | 1257 | | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
1258 | 1284 | | |
1259 | 1285 | | |
1260 | 1286 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
547 | 547 | | |
548 | 548 | | |
549 | 549 | | |
| 550 | + | |
| 551 | + | |
550 | 552 | | |
551 | 553 | | |
552 | 554 | | |
| |||
0 commit comments