Skip to content

Commit 1c9ccf1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-9g21fi
2 parents 3d90f3d + 6e11a92 commit 1c9ccf1

157 files changed

Lines changed: 3578 additions & 753 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-platform teaches the one effective read of a `.custom` declared override (ADR-0146 D8)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The `overrides: { package, basedOn }` declaration takes effect on every read of a name an installed package ships (ADR-0146 D8), but no published skill mentioned `.custom`, `overrides` or `basedOn`. An agent customizing an installed package (HotCRM) was still taught the Setup overlay row as the only path, which the read now treats as a package-unbound row awaiting migration, or wrote an undeclared same-name item that the layered read reports as an orphan.
11+
- **Now.** `objectstack-platform`'s Manifest Reference says where a customization lives (the paired `<base>.custom` package), what an author writes (the same-named item with `overrides: { package, basedOn }`, the base's `manifest.id` / `manifest.version`), where the declaration is honoured (only from the pair; today only for a view, dashboard, report, email_template or translation, the other overridable types waiting for their doors; `basedOn` never judged; any other package's declaration ignored) and what the read serves (every read agrees; one item per name in the environment-wide list; orphans in the layered read's `overlay`; a withdrawn public form never re-opened; the pair's export carries the declaration). One eval asserts the declared shape and refuses the Setup-write alternative.
12+
- The file stays under its token ceiling: the new text is paid for by deleting restatement in the same file (the CLI cheat sheet `references/operations.md` already holds, the seed `object`-is-derived sentence `objectstack-data`'s `seeds.md` holds, a table restating its own paragraph, two doubled rules). The `os migrate` row of `references/operations.md` takes back the `apply` rule the cheat sheet carried (loosens only; `--allow-destructive` for drops/tightenings). The SKILL.md ceiling lowers to its new measurement; no ceiling rises, no frontmatter, schema or behaviour change. The docs site gains the same facts on the metadata-lifecycle page and one sentence on the forms page.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest)!: an empty `?id=` on the app list and an empty `?object=` on the view list are refused 400 naming the parameter instead of being read as absent
6+
7+
Clause-②: no (narrowing) — `GET /meta/app` / `GET /meta/apps` with an empty `?id=`, and `GET /meta/view` / `GET /meta/views` with an empty `?object=` (on `RestServer` and on the runtime dispatcher, which share the list chain), are refused with `400 VALIDATION_ERROR`; they answered 200 with the caller's whole visible list before. Nothing is widened: no export, answer member, error code or status is added.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a list request whose filter names nothing is refused instead of served unfiltered. There is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing on the app and view list reads (`@objectstack/rest`).
13+
14+
- **Before.** `?id=` (app list) and `?object=` (view list) with an empty value were read as no filter, so a caller who asked for one app, or for one object's views, by a name that came out blank got every app or every view it may see (the views unsorted, and including views bound to no object).
15+
- **Now.** The list chain refuses either after the per-caller visibility gate: `400 VALIDATION_ERROR`, message `Query parameter 'id' names no app; …` or `Query parameter 'object' names no object; …` (the message names the parameter), and no list in the body. This is the answer #27484 gave a blank `$searchFields`.
16+
- **Unchanged.** A non-empty value narrows as before (one app or one object's views, or an empty list when it names none, including `' '`); no value lists every item the caller may see; a repeated `id` or `object` is still the arity 400; `?id=` on any type but `app`, and `?object=` on any type but `view`, are still ignored.
17+
- **Who sees it.** An API client that sends either parameter empty. `@objectstack/client` and the console send neither on these reads.
18+
19+
**The remedy.** Name the app or object, or leave the parameter out to list them all.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
feat(runtime)!: the permission-set clone door lands "new similar" in a writable target package — by default the source package's `.custom`, never package-less (#28563)
6+
7+
Clause-②: yes (narrowing) — `POST /security/permission-sets/:name/clone` takes an optional `package` and its answer names the package the copy landed in (widening); a copy of a set shipped by a read-only package now lands in that package's paired `<X>.custom`, ensured through the pair's one creation path, so a source whose package the ensure door refuses (it declares no namespace, its version is not SemVer, or another package owns the pair's namespace) is refused 409/422 where it was copied package-less before, and a given target that is unknown, read-only or out of reach is refused.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata key, value or type changes: the door's HTTP body gains an optional key and its answer gains one, and the copy's own definition is the same whole-definition copy as before. A refused clone writes nothing and its refusal names the remedy, so there is nothing stored for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the permission-set clone door (`@objectstack/runtime`).
13+
14+
- **Before.** Every copy was saved environment-wide with no package, whatever package shipped the set it was copied from. The body was `{ name, label }`.
15+
- **Now (ADR-0146 D3/D6).** The body is `{ name, label, package? }` and the answer is `{ permissionSet, package, notice }`.
16+
- `package` omitted, the set shipped by a read-only package X: the copy is saved in X's paired customization package `<X>.custom`, created through the ensure door (`ensureCustomizationPackage`) on the first copy and reused after. The stored row is bound to the pair; the copy's definition still names no package.
17+
- `package` omitted, the set already in a writable package (a base you own, or `<X>.custom`): the copy is saved in that package.
18+
- `package` omitted, the set authored in the environment: unchanged, environment-wide (`package: null`).
19+
- `package` given: it must be installed and writable (else 422 `WRITABLE_PACKAGE_REQUIRED`), and its dependency closure must reach the source's package and every object the set grants on (else 409 `RESOURCE_CONFLICT`). The rule is the one the flow clone door uses.
20+
- **Unchanged.** A new name is mandatory and a taken one is 409; `manage_metadata` and the walled catalog refusal answer first; the copy is assignable by its bare name through a position or a direct assignment.
21+
- **Who sees it.** An administrator copying a packaged set from Setup: the copy now travels with the package's customizations. A copy of a set from a package the ensure door cannot pair is refused with the ensure door's own sentence.
22+
23+
**The remedy.** For a refused pair: give the base package a namespace (and a SemVer version), or name a writable target with `package`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the automation skill's `mcp` recipe teaches the `providerConfig.toolsSnapshot` pin
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `providerConfig.toolsSnapshot` (a sha256 pin per MCP tool, checked on every materialization) shipped in `@objectstack/connector-mcp` and on the connectors page, but no published skill named it. An agent authoring an MCP connector app from the skills declared `include` alone, with no protection against a server silently rewriting a tool's description or `inputSchema`.
11+
- **Now.** `skills/objectstack-automation/references/integration-recipes.md`, recipe 2 (remote MCP), gains one paragraph: what `toolsSnapshot` pins and that a pinned name `include` leaves out is refused at boot; where the digests come from (`computeMcpToolsSnapshot(tools)`, or the served digest the degraded reason prints against a placeholder pin); what an author sees when a pin no longer matches (the instance degraded with no actions, `CONNECTOR_UPSTREAM_CHANGED`, the reason on `GET /api/v1/automation/connectors` naming each tool and the digest it now serves), and the link to the connectors page's pin section.
12+
- The file stays under its token ceiling: the paragraph is paid for by deleting restatements in the same file that keep a home elsewhere (the `connector_action` row of `SKILL.md`, its `api`-trigger *Signature* bullet, the objectstack-platform skill's `plugins:`/`requires` rule, and the connectors page). No ceiling moves, no frontmatter, schema, key or behaviour changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource)!: `POST /api/v1/datasources/test`, `POST /api/v1/datasources` and `PATCH /api/v1/datasources/:name` refuse every top-level key `DatasourceSchema` refuses, with that schema's own message, instead of dropping it
6+
7+
Clause-②: no (narrowing) — a test, create or edit body carrying a top-level key the datasource schema does not declare (a `password` on the test door; on all three doors a `url` or `host` one level too high, an alias such as `type`, a retired block such as `readReplicas`, or any unknown key), which the test door probed without and the write doors answered 201 / 200 with the key silently dropped, is refused with `400 DATASOURCE_ADMIN_ERROR`. Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: the datasource admin doors now refuse the top-level keys that `DatasourceSchema` already refuses at the build door, with that schema's own message. A refused body was never stored (the key was dropped), so there is nothing at rest for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`).
13+
14+
- **Before.** The unsaved-draft probe and the two write doors built their record from its known keys only. A top-level `password` on the probe vanished, so the probe tested a different connection than the one described. On all three doors any other key the strict schema refuses vanished the same way, and the write answered success.
15+
- **Now.** A top-level key `DatasourceSchema` does not declare is refused before anything is read, written or probed, naming the key and never its value. The message is the schema's own refusal, the one `os compile` prints: for example `url` belongs inside `config`, `type` is `driver`, `readReplicas` is retired. The test door answers under the heading `This connection draft is not a valid datasource.`; create and edit under `Invalid datasource 'NAME'.`. This supersedes the "other unknown top-level keys are still dropped as before" line of the #28549 entry.
16+
- **Unchanged.** Keys the schema declares pass the check, including the ones the doors do not store (`origin`, `description`, `ssl`, `autoConnect`). A credential sent as `secret` is still split off by the route and bound at `external.credentialsRef`. The probe's other answers (`ok: false` with a reason) are unchanged.
17+
18+
**The remedy.** Remove the key, or move it where the message says: connection keys into `config`, the credential into `secret`. Keys copied from a `GET /api/v1/datasources` or `GET /api/v1/datasources/NAME` answer that the schema does not declare (`hasSecret`, `redactedConfigKeys`, `definedIn`, `status`, `statusReason`) are not sent back on an edit.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): `GET /api/v1/packages/:id/customizations` answers over HTTP
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The read of what this deployment changed on an installed base (its `.custom` pair's overrides, the members the pair adds to the base's objects, and the base's switched-off items) shipped with its route-ledger row and its domain handler, but the dispatcher plugin never mounted it. So on a host without `@objectstack/hono`'s catch-all, every request to it answered the transport's 404. The dispatcher plugin now mounts it beside the other `/packages/:id/...` reads, at the unscoped prefix and at `/environments/:environmentId/packages/:id/customizations`.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the connector write door refuses an inline secret on every instance auth kind; the metadata read redacts connector credential positions
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: registered connector-instance-auth-inline-secret-refused -->
11+
12+
**BREAKING** accept-set narrowing on a provider-bound connector instance's `auth`, at every authoring door that parses `DeclarativeConnectorEntrySchema`: `defineStack({ connectors })`, `os validate` / `os compile`, and the metadata save door `PUT /api/v1/meta/connector/:name`.
13+
14+
- **Before.** The four static instance-auth kinds (`none`, `bearer`, `api-key`, `basic`) were plain objects: an undeclared key was dropped at parse and the entry still validated. The three ADR-0143 kinds (`oauth2`, `app-credential`, `signed`) already refused one.
15+
- **Now.** All seven kinds refuse an undeclared key with a located error at `auth` (`unrecognized_keys`; at the save door `422 INVALID_METADATA`, the envelope an inline `authentication` already met there). An inline secret — `token` on `bearer`, `key` on `api-key`, `password` on `basic`, and their usual spellings — meets the `credentialRef` prescription; `none` names no credential and says so. The refusal ADR-0143 D3 records for every variant now holds for the static kinds too.
16+
- **Read half (additive).** `connector` joins `datasource` as a built-in of the metadata-type redactor registry (`getMetadataTypeRedactor('connector')`), so every read exit that serves a stored body through that registry — the `/meta` item, list and layered reads and the metadata protocol's reads among them — withholds a stored connector's credential positions: every `auth` key its kind does not declare, every key of a runtime `authentication` block but `type`, and the provider header and environment maps (`providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers`, `providerConfig.transport.env`), each map whole. A save of the served form keeps the stored values (the metadata protocol's carry-forward), and the runtime keeps reading the unredacted execution view, so a materialized connector still sends its headers.
17+
- **Unchanged.** A static kind resolves its secret from `credentialRef` alone at materialization, as before; nothing ever read an undeclared key, so no connector's behaviour moves.
18+
19+
## FROM → TO
20+
21+
```ts
22+
// before — parsed green; the inline value was never used
23+
auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN', token: '…' },
24+
25+
// after — refused at `auth` with the credentialRef prescription; keep the reference only
26+
auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' },
27+
```
28+
29+
Set the secret under the name `credentialRef` gives — an environment variable in the open tier, or Setup → Connector credentials — and delete the inline key. There is deliberately no automatic rewrite: the inline value may be the only copy of the secret its author holds. The upgrade guide entry is `connector-instance-auth-inline-secret-refused`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
harden(data): an update by id answers the same refusal whether or not the id exists, as delete does
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
Skills-docs: none — no file under `skills/**` teaches what a by-id update answers for an id that names no record (a `git grep` of `skills/` for `RECORD_NOT_FOUND` names only the approvals record lock), so no skill line changes. The Update Record section of `content/docs/protocol/kernel/http-protocol.mdx` states the order.
10+
11+
The by-id update doors now judge the update before they look the record up — the order the by-id delete door already has.
12+
13+
- **What changes.** A caller the update is refused to — no edit permission on the object, or an external data source that refuses writes — now gets that refusal (`403 PERMISSION_DENIED`, `403 EXTERNAL_WRITE_FORBIDDEN`) for an id that names no record too, where it got `404 RECORD_NOT_FOUND`. This holds on `PATCH /api/v1/data/:object/:id`, on each row of `POST /api/v1/data/:object/updateMany` and of `POST /api/v1/data/:object/batch` with `operation: "update"`, and on every door that updates through them (the runtime dispatcher's `/data`, the `/mcp` HTTP tools, declarative update actions).
14+
- **What does not change.** A caller who may update the object still gets `404 RECORD_NOT_FOUND` for an id that names no record, and for a record its row-level access hides. Successful updates, the `405` for an operation the object does not expose, optimistic concurrency (`If-Match` / `expectedVersion`), upsert and system-context writes answer as before.
15+
- **Embedders calling the protocol directly.** `updateData`, `updateManyData` and `batchData`'s `update` operation no longer read the record before calling `engine.update`. They ask whether it exists only after the engine resolves a `null` readback, so an engine that resolves `null` for an id that names no record still gets `404 RECORD_NOT_FOUND`, and an update that moved its record out of the caller's row scope still answers `200` with `record: null`.

0 commit comments

Comments
 (0)