You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b9e6d50
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-security)!: under a `controlled_by_parent` declaration whose one master reference is `readonly` or `system`, every write that sets that reference is refused (ADR-0055 §1)
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
Metadata-protocol: none
10
+
11
+
<!-- adr-0087: not-required (already-registered cbp-master-relation-tiers-retired) registered before this change, and it covers it: that entry's stored-row listing (`os migrate meta --stored`) already reports this declaration as `master-not-writable`, with the prescription below. Nothing new moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed. What changes is the answer the security plugin's write gate gives on an object that reached the registry with a declaration the parse already refuses. -->
12
+
13
+
**BREAKING** for a caller that relied on such a write landing. A non-protocol change, shipped as `patch`.
14
+
15
+
ADR-0055 §1 derives a `controlled_by_parent` detail's access from exactly one required `master_detail` field that every write of a detail supplies. The parse refuses that master when it is declared `readonly: true` or `system: true`, so an object with it reaches the runtime only through a door that does not parse: a raw `registerObject` or `registerApp`, a `defineStack` call with `strict: false`, or a stored object the boot registers under its `[metadata_spec_invalid]` diagnostic. On such an object the security plugin's write gate let writes that set the master reference through, and a detail could end up outside the master its access derives from, readable by nobody, while the write reported success.
16
+
17
+
-**Before.** An insert under that declaration was written when it named a master. An update could set the reference.
18
+
-**Now.** Every write that sets the reference answers `INVALID_METADATA` (422) and writes nothing, before any master is asked: every insert, single or batch, whether or not it names a master, and every update whose body carries the reference key, whatever the value, `null` included. The message is the parse's own refusal of the declaration, prefixed with the write it refused, so it names the object, the field, each flag and the fix. It is the same for every caller the gate judges.
19
+
-**Unchanged.**
20
+
- The rows already stored: the security plugin still derives their read access from that field, so they keep their master and are read as before.
21
+
- A delete, and an update whose body leaves the reference key out.
22
+
- System contexts, every object declared with an ordinary required master, and every object that does not declare `controlled_by_parent`.
23
+
24
+
**What to do.** Fix the declaration as the parse says: remove `readonly: true` and `system: true` from the master reference and declare it as an ordinary required field. If the rows must outlive their parent, the object is not a detail: give it its own `sharingModel` (`private`, `public_read` or `public_read_write`).
fix(metadata-protocol)!: the metadata save doors and `composeStacks` refuse an object's `external` binding on a named managed datasource, from either side of the pair
7
+
8
+
Clause-②: yes (narrowing)
9
+
10
+
Metadata-protocol: narrowed
11
+
12
+
<!-- adr-0087: not-required (already-registered object-external-binding-managed-datasource-refused) registered by #25168 before this change, amended in this diff: its surface, replacement and reason now name composeStacks and the two metadata save doors -->
13
+
14
+
**BREAKING** (what the metadata API stores narrows), shipped as `minor` under the 18.x convention for breaking changes.
15
+
16
+
ADR-0015 §4.2 allows `object.external` only on a datasource whose `schemaMode` is not `'managed'` (an absent `schemaMode` is `'managed'` by the spec default). #25168 enforced the halves a parse and a single stack can see. A binding on a datasource stored or registered at runtime, or declared by another input of a composition, still got no verdict: the save door stored an `external` object on a managed datasource, stored a datasource re-saved as managed under objects bound to it with `external`, and `composeStacks` accepted the pair split across two inputs. The runtime then disagrees with itself about the object: its schema sync skips the table as federated while the boot validation and the write gate judge it as managed.
17
+
18
+
-**FROM:**`PUT /api/v1/meta/object/NAME` stored an object with `external` and a `datasource` naming a stored or registered datasource declared `schemaMode: 'managed'` or with no `schemaMode`, in draft and publish mode and under `force`. `PUT /api/v1/meta/datasource/NAME` stored a datasource as managed (or with no `schemaMode`) while stored or registered objects bound it with `external`. `composeStacks` accepted an `external` object of one input bound to a managed datasource another input declares.
19
+
-**TO:**
20
+
- The object save door refuses the object with `422 INVALID_METADATA`, the finding at `external`, nothing stored. The datasource it judges against is the one the door's own by-name read serves: the environment's stored row, else the definition the code layer registers.
21
+
- The datasource save door refuses the datasource with `422 INVALID_METADATA`, one finding per bound object at `schemaMode`, the stored row unchanged. The objects it judges are the environment's active object rows and the registered objects no stored row overlays. An object's draft binds nothing until it is published.
22
+
- Both are judged in draft and publish mode, and again when a draft is published (`publishMetaItem` and each item of a package publish), against the other half as it stands then. `force` does not skip them.
23
+
-`composeStacks` refuses the cross-input pair with `STACK_CROSS_REFERENCE_INVALID` (422), in either input order and under every `manifest` strategy, `preserve` included. An input built with `strict: false` that holds the pair itself is refused here too, as with the artifact pass's other rules.
24
+
- Every finding is the spec predicate's, so it reads as the parse's and `defineStack`'s do: "Object 'OBJECT' declares `external` on datasource 'NAME', whose `schemaMode` is 'managed' …".
25
+
-**Unchanged:** the rule itself. A binding on a datasource declared `schemaMode: 'external'` or `'validate-only'` is accepted on every door. A datasource no layer holds gets no verdict: the absence of a definition is no evidence about its `schemaMode`. The datasource-admin service's update (`PATCH /api/v1/datasources/NAME`) does not pass through the metadata save door and is not judged by this change. Rows already stored in the refused shape still load as before; a save of either side that leaves the pair inconsistent is refused, so the next object save waits until the datasource is external or the object drops `external`.
26
+
-**The fix for an author:** keep the pair consistent, in this order. To federate, save the datasource with `schemaMode: 'external'` (or `'validate-only'`) and its `external` settings first, then bind objects to it. To make a datasource managed, remove `external` from the objects that bind it first, then save the datasource.
27
+
28
+
This narrows the **Unchanged** bullet of the #25168 changeset, which said a binding on a datasource "registered by the host or another package, or stored at runtime" gets no verdict at the load doors: the metadata save doors now judge it, and `composeStacks` judges one another input declares.
29
+
30
+
`@objectstack/spec/data`: exports `externalBindingDatasourceRefusal` and its result type `ExternalBindingDatasourceRefusal`, the one predicate behind the rule, so a runtime door resolves the datasource it can see and asks it rather than copying the judgement. Its behaviour is unchanged.
fix(mcp)!: the `objectstack://objects` resources read the stdio transport's merged object view, so they list and describe every object `list_objects` and `describe_object` answer, and omit a disabled package's objects as those tools do
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is which definition the stdio MCP transport's two object resources read: the protocol's merged listing, the one its `list_objects` / `describe_object` tools and `GET /api/v1/meta/object` already serve, instead of the metadata service's copy. No published signature moves: `MCPServerRuntime.bridgeResources` keeps its two parameters, and the view reaches it through a package-internal function `index.ts` does not publish. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11
+
12
+
**BREAKING** (the object resources stop serving a disabled package's objects), shipped as `patch`: it changes which reads the stdio transport answers, not what a metadata author can write.
13
+
14
+
The long-lived stdio MCP server (`objectstack mcp`, `OS_MCP_STDIO_ENABLED`) answered `resources/read` of `objectstack://objects` and `objectstack://objects/NAME` from the `metadata` service. That service holds a copy of an object only where something put one there. On a kernel with an `environmentId` (`os dev`, `os serve`, `os start`, and the cloud single-environment runtime) nothing copies the objects a package registers during the boot. So `objectstack://objects` answered `{ objects: [], totalCount: 0 }` for such a package and `objectstack://objects/NAME` answered `404 RESOURCE_NOT_FOUND` for each of its objects, while `list_objects` and `describe_object` listed and described them.
15
+
16
+
-**Before.** On that kernel, an object a package registered during the boot was missing from `objectstack://objects`, and `objectstack://objects/NAME` answered `{ error: 'Object "NAME" not found', code: 'RESOURCE_NOT_FOUND', status: 404 }` for it.
17
+
-**Now.** Both resources read the view `list_objects` and `describe_object` answer from: the protocol's merged listing, the one `GET /api/v1/meta/object` serves. `objectstack://objects/NAME` answers the same body `describe_object` answers for that name.
18
+
-**Also changed.** A disabled package's objects, where the `metadata` service still held a copy, were listed by `objectstack://objects` and described by `objectstack://objects/NAME`. They are now omitted, and `objectstack://objects/NAME` answers `404 RESOURCE_NOT_FOUND` for them, as `describe_object` answers "not found". When the protocol's merged listing throws, both resource reads fail with that error, as `list_objects` does, instead of answering the `metadata` service's listing.
19
+
-**Unchanged.** Objects that already had a copy (every object on a kernel without an `environmentId`, a marketplace install on any kernel, and a runtime-authored object) are listed and described as before. The completeness verdict is still the `metadata` service's: a loader outage still withholds `totalCount` and answers `partial: true`, and a miss during an outage still answers `503 SERVICE_UNAVAILABLE`. A host with no `protocol` service, or one without the merged listing, keeps reading the `metadata` service. A host that calls `MCPServerRuntime.bridgeResources` itself reads the `metadata` service as before.
20
+
-**What to do.** Nothing. A stdio client that read a disabled package's object through these resources was reading a stale copy the rest of the platform no longer serves: re-enable the package to restore it, as REST says.
| 413 |`validation`|`PAYLOAD_TOO_LARGE` — an upload over the storage settings' `max_upload_mb`, or an import over its row ceiling (see [above](#payload_too_large)) |
976
-
| 422 |`validation`|`MISSING_REQUIRED_FIELD` on an absent `controlled_by_parent` master reference (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
981
+
| 422 |`validation`|`MISSING_REQUIRED_FIELD` on a `controlled_by_parent`detail whose stored master reference is null (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
0 commit comments