Skip to content

Commit 9b79885

Browse files
committed
Merge origin/main into claude/issue-19228-view-row-ceiling-retire
Two hand-written paths conflicted; both are resolved so each side's intent stands. Generated artifacts are regenerated in the next commit. packages/spec/src/type-alias-convention.pin.test.ts: not resolved by hand. Main re-keyed every isomorphic pin from the IsoNNN counter to a name derived from module path and schema. This branch's pre-merge copy of the file was re-keyed with that change's own rekey.mjs transform (controls: it reproduces ece9f71 from fdeeea0 byte for byte, and is idempotent on main's copy), then main's prose half was re-applied with git merge-file against ece9f71 / fc8eda2. The one conflicting hunk is the tail of the count history: both entries are kept, 789 -> 789 first, then 789 -> 790. The new pin is Iso_ui_view__KanbanConfigSchema, and the ui/view note names it and no longer says a numeral stays vacant. readIsomorphicPins: 790 = 790 against the branch's pre-transform file, set difference 0 both ways, 0 bodies changed, 0 names off the rule, sorted; against main, +1 (ui/view.zod.ts::KanbanConfigSchema). packages/spec/src/ui/view.zod.ts: this branch deletes the rowLimitKey helper and its docblock with the retired per-kind view limit; main re-measured one citation paragraph inside that docblock at the new console pin. The docblock describes a key that no longer exists, so it goes; the rest of main's changes to the file merged cleanly and stay. The branch's delta against main now equals its delta against the merge base, line for line, except that the deleted paragraph is main's re-measured text. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
2 parents ff13eec + 61609ed commit 9b79885

562 files changed

Lines changed: 44853 additions & 18877 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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
'@objectstack/plugin-webhooks': minor
3+
---
4+
5+
fix(plugin-webhooks): a webhook credential stored as cleartext inside `sys_webhook.definition_json` is refused, at the delivery path and at the write door (#10164)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner and the ADR-0087 disposition below, never by the level).
12+
13+
**Webhooks that still carry the legacy cleartext shape STOP DELIVERING.** A
14+
`sys_webhook` row whose signing secret or custom header map exists only as a
15+
`secret` / `headers` key inside `definition_json` — with nothing stored in the
16+
encrypted `signing_secret` / `headers_secret` column — used to be delivered from
17+
that cleartext with a `warn`. It is now refused, dated `2026-09-23`:
18+
19+
- **Delivery path.** The subscription is PARKED, the same fail-closed shape as an
20+
encrypted credential that cannot be recovered: nothing is sent, and every
21+
matching record change is recorded in `sys_http_delivery` as a `dead` row with
22+
0 attempts, no signature and no headers. Its `error` names the refusal as
23+
`[VALIDATION_ERROR/400]`, and the drop is reported once at `error`, with
24+
`code: 'VALIDATION_ERROR'`, `status: 400`, `field: 'definition_json'` and the
25+
refused `keys` in the log meta.
26+
- **Write door** (when `WebhookOutboxPlugin` is mounted, the standard mount). A
27+
`sys_webhook` insert or update whose `definition_json` carries a `secret` or
28+
`headers` key, whatever its value, is refused before anything is stored
29+
(`VALIDATION_ERROR` / `400`, with `object`, `field` and `keys` on the error).
30+
That covers a raw `PATCH /api/v1/data/sys_webhook`. It also covers a Setup-form
31+
save that echoes back a legacy blob unchanged. Omitting `definition_json`, or
32+
writing one without those keys, is unaffected. The plugin binds this refusal
33+
itself, and it is not exported from the package entry: a host that composes
34+
`AutoEnqueuer` on its own still gets the delivery-path refusal above, but not
35+
this write-door refusal.
36+
37+
**Fix.** Both remedies need a registered `CryptoProvider`
38+
(`engine.setCryptoProvider` — `LocalCryptoProvider` in dev, KMS/Vault in
39+
production), because writing a `secret`-typed column is itself refused without
40+
one. With a provider registered, either:
41+
42+
- restart, and the boot sweep `migrateLegacyWebhookSecrets` moves both values into
43+
their encrypted columns and strips them from `definition_json` in one update. The
44+
subscription re-arms at the next refresh.
45+
- or re-author the webhook yourself. Write the key into `signing_secret` and the
46+
header map into `headers_secret` (a JSON object of string values), then remove
47+
both keys from `definition_json`.
48+
49+
There is no transition path for a deployment that runs with no `CryptoProvider`.
50+
51+
Unchanged: authoring. `defineWebhook({ secret, headers })` is written exactly as
52+
before, and the boot materializer still routes each value to its encrypted column.
53+
A row whose encrypted columns are set delivers exactly as before.
54+
55+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: `packages/spec` is untouched, `WebhookSchema` still declares `secret` and `headers`, and the materializer still routes them to their encrypted columns, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What is refused is a stored DATA-row shape on `sys_webhook`, and its converter already ships and runs at every boot: `migrateLegacyWebhookSecrets`, named in the refusal and in the Fix paragraph above. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a data-row sweep (not `registered` / `already-registered`); and the change is runtime behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`composeStacks(…, { manifest: 'preserve' })` emits each definition **once**, in the body of the package that owns it: a multi-package release artifact no longer carries a flattened copy of its collections at the top level (#14512, ADR-0130 D4's 2026-09-22 addendum).
6+
7+
Measured on `examples/app-multi-package` (two packages, three definitions): `dist/objectstack.json` goes from 8,223 to 5,328 bytes, −35%, and the ratio does not improve with size — it was one extra copy of everything a package owns. The artifact's own `manifest`, `packages`, `plugins`, `devPlugins`, `devLogins`, `api`, `server`, `i18n`, `runtimeModule`, `onEnable` and `devHint` are untouched at the top level; the 35 package-owned collections are what moves.
8+
9+
**BREAKING** for anything that read a compiled multi-package artifact's top-level collections directly. Every reader the platform ships was converted first — the ruling's order was readers first, emitter last — and this lands only with #15004's acceptance probe green on both shapes, which boots a two-package collection zoo through all five load boundaries and fails if any subsystem sees an empty collection.
10+
11+
- **No authored metadata changes, and no artifact on disk is invalidated.** The authoring shape is identical: N `defineStack` packages plus a project config composing them with `manifest: 'preserve'`. ADR-0130 D4's read-both rule is untouched — `packages` present ⇒ iterate it, `packages` absent ⇒ `manifest` as a single-element list — so an artifact built before this change, which carries both halves, loads exactly as it did.
12+
- **A single-package artifact keeps today's shape byte for byte** (ADR-0130 D7): the emitter strips nothing below two package entries.
13+
- **The flattened half is dropped only where it is a COPY — three conditions, and a composition failing any one of them keeps today's additive shape**: (1) two or more package entries; (2) every input's collections attributed to a body — an input declaring no `manifest` has no package that could own its collections, and an input already carrying `packages` contributes those entries untouched; (3) the bodies reproduce the flattened collections item for item. Condition 3 is what `objectConflict: 'merge'` / `'override'` and a standalone action bound onto a SIBLING package's object fail: composition RECONCILES those into a top-level definition no body carries, so the flattened half is not a copy of anything and stays. Nothing here is refused — a composition that was legal before stays legal.
14+
- **Why the copy goes rather than being compressed**: one definition was serialized twice with nothing keeping the copies equal. Where they differ today the flattened one is the reconciled copy and the platform's reader prefers it deterministically, which is exactly why this change removes the second copy only where the first one is redundant. One measured consequence on the compiled path: a seed dataset declared once reached the runtime's shared seed registry twice, and now reaches it once.
15+
16+
Clause-②: yes (narrowing)
17+
18+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves: no spec key, export or config field is removed or renamed, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite. What changed is the SHAPE A COMPILER EMITS, and every reader of it in this repository was converted by the reader half of the same ruling (#15005, #15006, #15007, #15229, #15232) before this landed. -->

‎.changeset/15178-translation-bundle-split-settings-platform-only.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ and the rejection carries the prescription above.
6666

6767
### Unchanged
6868

69-
The registered `translation` metadata type (`TranslationItemSchema`) still
70-
declares `settings` — this ruling covers the file-authored bundle. `GET
69+
The registered `translation` metadata type (`TranslationItemSchema`) is not
70+
changed by THIS entry — this ruling covers the file-authored bundle. (Superseded
71+
in the same release: #19620 narrows the item door too; see its own changeset.) `GET
7172
/api/v1/i18n/translations/:locale` still declares it on its response, because the
7273
served document is the merged tree; `GetTranslationsResponseSchema` is typed
7374
against the platform face for exactly that reason.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`ListViewSchema` declares the `columns` x `hiddenFields` x `fieldOrder` composition instead of leaving it to be inferred from a renderer (#15184)
6+
7+
The three keys that together build a list view's field list now say how they compose, in their own `.describe()` text — the string that ships into `json-schema/` and into the generated `content/docs/references/ui/view.mdx`:
8+
9+
- `columns` is the **projection**: the candidate set and the baseline order, and neither other key can add a field it omits. An **empty** `columns` declares no projection, so neither other key applies: which columns show is left to the renderer (objectui's `ListView` grid derives the object's default columns).
10+
- `hiddenFields` **subtracts** from that projection, before any ordering runs. A name `columns` never projected subtracts nothing.
11+
- `fieldOrder` **orders what survives** and never adds a field. A surviving column absent from `fieldOrder` sorts **last**, after every listed one, keeping its `columns`-relative order; a name listed there that did not survive orders nothing.
12+
13+
**Why this is a declaration and not a precedence rule.** `fieldOrder` was proposed for retirement as a second spelling of `columns` with no contract deciding who wins. They never compete: one selects, the other sorts. Maintainer decision batch #115 (2026-09-11) kept the key and ruled the composition into the contract, which is what this change lands.
14+
15+
⛔ **No accept set moves.** No key is added, removed, narrowed or widened; no parse verdict changes; the four `@objectstack/lint` list-view validators are untouched. What changes is the published description of three keys that were already there, plus one ledger row's evidence.
16+
17+
**`packages/spec/liveness/view.json` — the `/props/list/children/fieldOrder` row is re-cited**, `verifiedAt: 2026-09-21`. The ledger ships in this package's `files[]`, so the pointers an upgrading reader follows are these, and both halves of the 2026-08-10 citation had rotted: its first path (`objectui packages/react/src/spec-bridge/bridges/list-view.ts`) no longer exists, and its second had drifted in range through three sets of line numbers. The row now anchors both pointers on symbols, splits the relay rung out into `producer`, and names the measurement it was taken at.
18+
19+
The declaration and the accept set are held together by `packages/spec/src/ui/view-field-order-composition.pin.test.ts`: it reads the three descriptions off the live schema and parses a document carrying all three keys through the page-list, object-views, `defineView` and registered-metadata doors, asserting the arrays come back verbatim — the spec declares the composition, it does not perform it.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
A release artifact whose `packages` is present but is not an array (`{}`, `0`, `'x'`) is now refused by the runtime's collection reader too, as `INVALID_ARTIFACT_PACKAGES` (ADR-0112, `status: 422`) (#15293).
6+
7+
Clause-②: no
8+
9+
`packages` is declared as an array of package entries (`ObjectStackDefinitionSchema.packages: z.array(ArtifactPackageSchema).optional()`), and the rule is now written down once, beside `AssembledPackageBodySchema` in `@objectstack/spec`: an absent `packages` means a single-package artifact, and any other non-array value is malformed and refused. `resolveArtifactPackageOrder` in `@objectstack/core` already refused it, and so did the i18n detector in `@objectstack/plugin-dev` and the default-permission-set reader in `@objectstack/plugin-security`.
10+
11+
- **What changes**: `AppPlugin` reads its collections in `start()`, and `start()` now raises the same refusal `init()` already raised through the kernel's `manifest` service. Under `os dev`, `DevPlugin`'s child-`start()` loop logs it on its `error` line, where before the app started on its top-level collections alone. `createStandaloneStack` now refuses such an artifact while it builds the stack. Before, the refusal came later, when the app registered with the `manifest` service. `loadArtifactBundle`'s runtime-module merge reports it through its existing `warn` line and skips the merge, as it already does for a malformed `packages[]` entry. `resolveProjectDatabaseUrl` no longer reads a default datasource out of such an artifact: it declines, as it already does for any artifact it cannot read, and moves on to the next rung (the unified default database). The boot that loads the artifact then refuses it.
12+
- **What does not change**: an absent `packages`, and `packages: null`, still return the caller's own object by identity. A well-formed `packages[]` resolves exactly as before.
13+
- **Fix**: remove the `packages` key for a single-package artifact, or make it an array of `{ manifest: … }` entries.

‎.changeset/15705-mcp-resume-run.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/mcp': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
feat(mcp): `resume_run` continues a flow run that paused on a screen, behind the same gates as `run_action` (#15705)
7+
8+
Clause-②: yes
9+
10+
**What changed.** `run_action` on a flow action whose flow stops on a `screen` node answers `status: "paused"` with a `runId` and the `screen` to fill in. Until now nothing on the MCP surface could submit that screen, so the run stayed parked: an agent could start such an action but never finish it. The new MCP tool `resume_run({ runId, values?, confirm? })` submits the screen's field values (keyed by the names in `screen.fields`) and the run continues. It answers with `run_action`'s envelope, `{ ok, action, objectName, recordId?, result }`. A run that pauses on its next screen comes back paused again, so a multi-screen wizard is walked by calling `resume_run` once per screen.
11+
12+
**Which runs it continues, and no others.** The runtime's bridge admits a call only where `run_action` would admit starting the same flow on the same record for this caller now:
13+
14+
- **Only the caller's own run.** The run's trigger identity must be the caller. Another user's run, an unknown id and a finished run all answer the same `404 RESOURCE_NOT_FOUND`. A resumed run continues under the identity of the user who started it, so only that user may continue it.
15+
- **`run_action`'s gates, with `run_action`'s helpers.** A `type: 'flow'` action whose `target` is the run's flow, on the run's object, must be AI-exposed (`ai.exposed`), must pass the caller's `requiredPermissions` and must not be switched off (`ACTION_DISABLED`, `409`). An action flagged `ai.requiresConfirmation` needs `confirm: true` on the resume too (`ACTION_CONFIRMATION_REQUIRED`, `428`), because the flow's writes happen after the screen. The exposure and permission refusals answer `403 PERMISSION_DENIED`. So does a run that no flow action targets.
16+
- **The subject record is read again as the caller.** A record the caller can no longer read is refused `404 RECORD_NOT_FOUND`, which is how `run_action` refuses it.
17+
- **Screen pauses only.** A run waiting on anything else (a timer `wait`, an approval) is refused `409 RESOURCE_CONFLICT` and left as it is.
18+
19+
Every refusal happens before the engine is asked, so the run stays parked. The engine's own answers (a screen submission missing a required field, a concurrent resume, a run that resumed and then failed) reach the caller with the code, status, message and `details` that `POST /api/v1/automation/:name/runs/:runId/resume` gives for the same result. The two doors now share one classification of the engine's answer. It was moved out of the REST route unchanged, and the route's answers are byte-identical.
20+
21+
**For hosts.** `McpActionBridge` gains an OPTIONAL member, `resumeRun(runId, { values?, confirm? })`. A bridge that implements it gets `resume_run` beside `run_action`, under the same `actions:execute` OAuth scope, on both the HTTP and the stdio transport. A bridge without it is unchanged and does not list the tool. `run_action`'s description names `resume_run` only where it is registered. `@objectstack/runtime`'s MCP bridge implements the member.

0 commit comments

Comments
 (0)