Skip to content

Commit 4b0508e

Browse files
claude[bot]claude
andauthored
docs(runtime,metadata-protocol): the scope-less booted row is a marketplace / offline import, not a multi-package artifact's module (#16123)
* docs(runtime,metadata-protocol): the scope-less booted row is a marketplace / offline import, not a multi-package artifact's module Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * chore(changeset): record the scope-less-row attribution correction for the next release notes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * fix(docs): re-anchor the system-context census row 52 onto the line the elevation read moved to Pure line rot from this PR's own comment expansion in packages/runtime/src/domains/packages.ts: the shouldDenyAnonymous read moved from :535 to :543, so row 52's anchor and the read no longer met. Written by `node scripts/check-system-context-census.mjs --fix`; the anchor's number is the only thing that changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b45c3f6 commit 4b0508e

6 files changed

Lines changed: 90 additions & 22 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
docs(runtime,metadata-protocol): correct the `writable` verdict's illustration — the scope-less booted row is a marketplace / offline import, never a multi-package artifact's module (#14803)
7+
8+
Comment and prose only. No predicate, no assertion and no served shape changes;
9+
every pin behind the `writable` verdict stays green as written.
10+
11+
The `writable` verdict shipped in 17.3.0 with a **false attribution** in its own
12+
explanation, and this corrects it at every site that repeated it. The claim was
13+
that the scope-less booted row `isWritablePackage` answers `false` for is *the
14+
`type: module` sub-package a multi-package artifact carries*. It is not, and it
15+
never was:
16+
17+
- `defineStack` parses every `packages[]` entry through `ManifestSchema`
18+
(`spec/src/stack.zod.ts`, `ArtifactPackageEntrySchema`), whose `scope` is
19+
`.default('project')` (`spec/src/kernel/manifest.zod.ts`), so **no** package of
20+
a compiled artifact is ever scope-less — `dist/objectstack.json` and both
21+
served rows carry `scope: "project"`.
22+
- A genuinely scope-less row arises only where a manifest reaches the registry
23+
**without** that parse, because `installPackage` stores a key-by-key copy that
24+
applies no defaults: a marketplace install / offline file import
25+
(`manifestService.register(rawBody)` to `ql.registerApp`) for the **booted,
26+
read-only** half, and `POST /api/v1/packages` (`body.manifest || body` to
27+
`installPackage`) for the **database base, writable** half.
28+
29+
Measured: `ManifestSchema.parse` of the `app-multi-package` orders body turns an
30+
unauthored `scope` into `scope: "project"`, while `SchemaRegistry.installPackage`
31+
of the same unparsed body yields a record with no `scope` key at all.
32+
33+
What stays, because it is true and load-bearing: a scope-less **booted** package
34+
is read-only while a scope-less **database base** is writable, and only
35+
`engine.manifests` tells them apart — which is why the server owns the verdict.

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ The largest single consumer — **17 of the 105 sites**.
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161161
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5231`, `:6657`, `:6905`, `:7336`, `:7529` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163-
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:535`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
163+
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:543`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
165165
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` |
166166
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:241`, `:274` |

packages/metadata-protocol/src/get-meta-items-package-writable.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
* enforce — #8146's "one answer to 'is this package writable?'" applied to the
1111
* read side. It reads `engine.manifests` FIRST: a package booted from an
1212
* artifact through `registerApp` is read-only whatever its scope says, and a
13-
* scope-less `type: module` carried by a multi-package artifact lands there
14-
* too, while a scope-less Studio-created base does not. Only the server holds
13+
* scope-less BOOTED package — a marketplace install / offline file import,
14+
* which reaches the registry with no `ManifestSchema` parse — lands there
15+
* too, while a scope-less Studio-created base (`POST /api/v1/packages`) does
16+
* not. ⛔ Neither is a module carried by a multi-package artifact:
17+
* `defineStack` parses every `packages[]` entry through `ManifestSchema`, whose
18+
* `scope` is `.default('project')`, so no package of a compiled artifact is
19+
* ever scope-less. Only the server holds
1520
* `engine.manifests`, which is why the client could never derive this.
1621
*
1722
* The engine is the same shape `meta-overlay-cache.test.ts` drives: the
@@ -24,7 +29,7 @@ import { ObjectStackProtocolImplementation } from './protocol.js';
2429

2530
/** Booted code package, explicit `scope: 'project'`. */
2631
const CODE_PROJECT = 'app.acme.crm';
27-
/** Booted, SCOPE-LESS module — the multi-package-artifact sub-package. */
32+
/** Booted and SCOPE-LESS — a marketplace / offline import, registered unparsed. */
2833
const CODE_MODULE = 'app.acme.crm.billing';
2934
/** Platform / marketplace delivered. */
3035
const SYSTEM_SCOPED = 'com.objectstack.platform';
@@ -47,7 +52,9 @@ function make() {
4752
row(DB_BASE),
4853
];
4954
const byId = new Map(records.map((r) => [r.manifest.id as string, r]));
50-
// What `ObjectQL.registerApp` records for every package of a loaded artifact.
55+
// What `ObjectQL.registerApp` records — for every package of a loaded artifact
56+
// (CODE_PROJECT, parsed and therefore `scope: 'project'`) and for a
57+
// marketplace / offline import (CODE_MODULE, unparsed and therefore scope-less).
5158
const manifests = new Map<string, unknown>([
5259
[CODE_PROJECT, byId.get(CODE_PROJECT)!.manifest],
5360
[CODE_MODULE, byId.get(CODE_MODULE)!.manifest],

packages/metadata-protocol/src/protocol.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7320,9 +7320,14 @@ export class ObjectStackProtocolImplementation implements
73207320
// package switcher reads that list; it used to derive "writable"
73217321
// client-side from `manifest.scope` alone, which is not this server's
73227322
// rule — ADR-0070 D2 (`isWritablePackage`) reads `engine.manifests`
7323-
// FIRST, so a scope-less module booted from a multi-package artifact is
7324-
// read-only while a scope-less Studio-created base is writable, and only
7325-
// the server can tell the two apart. Same predicate the authoring and
7323+
// FIRST, so a scope-less BOOTED package — a marketplace install / offline
7324+
// file import, which reaches the registry through `ql.registerApp` with no
7325+
// `ManifestSchema` parse — is read-only while a scope-less Studio-created
7326+
// base (`POST /api/v1/packages`) is writable, and only the server can tell
7327+
// the two apart. ⛔ Neither is a module carried by a multi-package
7328+
// artifact: `defineStack` parses every `packages[]` entry through
7329+
// `ManifestSchema`, whose `scope` is `.default('project')`, so no package
7330+
// of a compiled artifact is ever scope-less. Same predicate the authoring and
73267331
// lifecycle gates use (#8146: one answer), computed on a spread COPY:
73277332
// the registry record is never mutated and the verdict is never stored.
73287333
// The runtime dispatcher door decorates its own read of the same

packages/runtime/src/domains/packages-writable-verdict.test.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
* `isWritablePackage` (ADR-0070 D2), and it is a different predicate: it reads
1212
* `engine.manifests` FIRST — a package booted from an artifact through
1313
* `registerApp` is read-only whatever its scope says — and only then the
14-
* `system` / `cloud` scopes. The two rules split on exactly the row ADR-0130
15-
* introduces: a scope-less `type: module` carried by a multi-package artifact
16-
* is in `engine.manifests` (read-only) while a scope-less Studio-created base
17-
* is not (writable). Nothing in the raw row distinguishes them; `engine.manifests`
14+
* `system` / `cloud` scopes. The two rules split on the scope-less pair: a
15+
* scope-less BOOTED package is in `engine.manifests` (read-only) while a
16+
* scope-less Studio-created base is not (writable). ⛔ Neither half of that
17+
* pair is a module carried by a multi-package artifact: `defineStack` parses
18+
* every `packages[]` entry through `ManifestSchema`, whose `scope` is
19+
* `.default('project')`, so no package of a compiled artifact is ever
20+
* scope-less. A row is scope-less only where it reached the registry WITHOUT
21+
* that parse — a marketplace install / offline file import for the booted
22+
* half, `POST /api/v1/packages` for the base. Nothing in the raw row
23+
* distinguishes them; `engine.manifests`
1824
* does, and only the server holds it. So the server says it.
1925
*
2026
* ## What is asserted
@@ -36,9 +42,14 @@ import { HttpDispatcher } from '../http-dispatcher.js';
3642
/** Pin 1 — booted code package, explicit `scope: 'project'` (today's hotcrm shape). */
3743
const CODE_PROJECT = 'app.acme.crm';
3844
/**
39-
* Pin 2 — booted code package with NO scope key: the `type: 'module'` sub-package a
40-
* multi-package artifact carries (ADR-0130 D4/D5). The raw body is what the
41-
* load path registers (D7), so the row has no `scope` at all. THE row #14375
45+
* Pin 2 — booted code package with NO scope key: a marketplace install / offline
46+
* file import, whose RAW body reaches the registry through
47+
* `manifestService.register(rawBody)` → `ql.registerApp` with no
48+
* `ManifestSchema` parse, so the row has no `scope` at all. ⛔ Not the
49+
* sub-package a multi-package artifact carries — `defineStack` parses every
50+
* `packages[]` entry through `ManifestSchema`, whose `scope` is
51+
* `.default('project')`, so no package of a compiled artifact is ever
52+
* scope-less. THE row #14375
4253
* exists for: the client heuristic said "writable"; the server says read-only.
4354
*/
4455
const CODE_MODULE = 'app.acme.crm.billing';
@@ -72,8 +83,10 @@ function make() {
7283
registry.installPackage(manifest(DB_BASE));
7384
registry.installPackage(manifest(DB_PROJECT, { scope: 'project' }));
7485

75-
// Only the two code packages booted from an artifact — this is what
76-
// `ObjectQL.registerApp` records for every package of a loaded artifact.
86+
// Only the two code packages are booted — this is what `ObjectQL.registerApp`
87+
// records, both for every package of a loaded artifact (which is CODE_PROJECT,
88+
// parsed and therefore `scope: 'project'`) and for a marketplace / offline
89+
// import (CODE_MODULE, unparsed and therefore scope-less).
7790
const manifests = new Map<string, any>([
7891
[CODE_PROJECT, manifest(CODE_PROJECT, { scope: 'project', type: 'app' })],
7992
[CODE_MODULE, manifest(CODE_MODULE, { type: 'module' })],
@@ -117,7 +130,7 @@ describe('GET /packages — every row carries the server\'s writable verdict (#1
117130
expect(byId(rows, CODE_PROJECT).writable).toBe(false);
118131
});
119132

120-
it('pin 2: a booted, SCOPE-LESS module (multi-package artifact sub-package) is writable: false', async () => {
133+
it('pin 2: a booted, SCOPE-LESS package (marketplace / offline import) is writable: false', async () => {
121134
const rows = await list(make().dispatcher);
122135
const row = byId(rows, CODE_MODULE);
123136
// The raw row really has no scope — the verdict is not coming from it.

packages/runtime/src/domains/packages.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -492,10 +492,18 @@ function requireDuplicableSource(
492492
* `manifest.scope` alone (`scope !== 'project'`). That is not the rule this
493493
* server enforces: {@link isWritablePackage} (ADR-0070 D2) reads
494494
* `engine.manifests` FIRST — a package booted from an artifact through
495-
* `registerApp` is read-only whatever its scope says, and a scope-less module
496-
* carried by a multi-package artifact (ADR-0130 D4/D5) lands there too. The
497-
* client cannot see `engine.manifests`, so it cannot tell that module
498-
* (read-only) from a scope-less Studio-created base (writable); only the
495+
* `registerApp` is read-only whatever its scope says, and a scope-less BOOTED
496+
* package lands there too. ⛔ That scope-less row is NOT a module carried by a
497+
* multi-package artifact: `defineStack` parses every `packages[]` entry through
498+
* `ManifestSchema` (`spec/src/stack.zod.ts`, `ArtifactPackageEntrySchema`),
499+
* whose `scope` is `.default('project')`, so no package of a compiled artifact
500+
* is ever scope-less. A row reaches the registry scope-less only WITHOUT that
501+
* parse: a marketplace install / offline file import
502+
* (`manifestService.register(rawBody)` → `ql.registerApp` — booted, hence
503+
* read-only), or a Studio-created base through `POST /api/v1/packages`
504+
* (`body.manifest || body` → `installPackage`, which stores a key-by-key copy
505+
* and applies no defaults — hence writable). The client cannot see
506+
* `engine.manifests`, so it cannot tell those two apart; only the
499507
* server can, so the server says it — with the SAME predicate the authoring
500508
* and lifecycle gates use, which is #8146's ruling ("one answer to 'is this
501509
* package writable?'") applied to the read door.

0 commit comments

Comments
 (0)