Skip to content

Commit 59e0b7e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-ybjm3h
2 parents dd85dd1 + be78977 commit 59e0b7e

59 files changed

Lines changed: 2928 additions & 715 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(storage)!: an anonymous write carries no file
6+
7+
Ruling C ("an anonymous submission carries no file") now holds at every anonymous write door of ADR-0138 D2, not only at the public form door. A record write whose caller has no user, such as the guest an `authRequired: false` endpoint executes as under the grants a deployment binds to the guest, may not put a value in a file field (`file`, `image`, `avatar`, `video`, `audio`). It is refused before anything is written, with the form door's envelope: `400 VALIDATION_FAILED`, one `fields[]` entry per refused field carrying the ADR-0114 field code `invalid_value` and the field's label. An empty or absent file field is written as before. Signed-in callers, system writes and context-less programmatic writes are unchanged. No error code is added.
8+
9+
Clause-②: no (narrowing) — an anonymous write that carried a value in a file field is refused. Nothing that was refused becomes accepted.
10+
11+
Metadata-protocol: none (a write path's file rule; nothing a metadata author writes changes)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No metadata 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, so there is nothing for `objectstack migrate meta` to rewrite. What changes is which record writes the file-reference path accepts (an anonymous caller's write carrying a file value is refused). The remedy is a change of who provides the file (a signed-in caller), never a rewrite of a package source or a stored row. 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). -->
14+
15+
**BREAKING** for an anonymous write that carried a file value, shipped as `patch` because the metadata protocol does not move.
16+
17+
- **Before.** Only the public form door refused a file value from an anonymous caller.
18+
- **Now.** Every write whose caller has no user is refused with `400 VALIDATION_FAILED` when a file field carries a value.
19+
- **Fix.** Leave file fields empty in an anonymous write. A file is provided by a signed-in caller, through the authenticated upload doors.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/objectql': patch
4+
'@objectstack/service-package': patch
5+
'@objectstack/cloud-connection': patch
6+
'@objectstack/metadata-core': patch
7+
---
8+
9+
feat(install): a package that declares an installed app in `manifest.dependencies` may share that app's namespace, and a boot hydrates installed packages in dependency order (ADR-0141 D5)
10+
11+
Clause-②: yes (narrowing)
12+
Metadata-protocol: none
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is added, removed, renamed or re-shaped: `manifest.dependencies` keeps its record shape and meaning. The one narrowing is an install-local acceptance detail of the not-yet-released ADR-0141 D2 check (an artifact wrapper's own id no longer counts as a delivered package), plus the matching removal of the unreleased `DeclaredDependencyCheck.deliveredIds` option; there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
15+
16+
**Namespace co-ownership by declaration.** The install gate (`SchemaRegistry.installPackage`) used to admit packages into one namespace only when one release artifact delivered them together. A module shipped as its own artifact that uses its app's namespace was refused `422 NAMESPACE_CONFLICT` beside the installed app, whatever it declared. Now an installed package P co-owns namespace N with:
17+
18+
- an installed X that P declares in `manifest.dependencies`, when both use N;
19+
- a sitting owner of N that declares P, which is the same relation read from the other end, so an upgrade or reinstall of the app beside its modules is admitted;
20+
- a sibling S, when S and P each declare one installed package that holds N (two modules of one app, neither declaring the other).
21+
22+
Only direct declarations count. The shared parent must be installed and must hold N itself.
23+
24+
The object-name check reads the same co-owner set. A co-owner by declaration that defines an object name another co-owner already owns is refused `422 DUPLICATE_ARTIFACT_OBJECT_NAME` before any DDL. Examples: a module redefining its app's object, a sibling redefining the other sibling's object, or an app upgrade newly defining a name one of its modules owns. The message is worded per source (same artifact, declared dependency, sibling). `NAMESPACE_CONFLICT` now names the declaration as the fix for a package that extends the app holding the namespace.
25+
26+
**Hydrate order.** The `sys_packages` hydrate and the install-local `kernel:ready` rehydrate used to register in source-listing order. Now they sort their batch by `manifest.dependencies`, with declared ids as optional edges, through `resolvePluginOrder`. Before this, a module that hydrated before the app whose namespace it shares could be refused and stay absent for the process lifetime. In the install-local rehydrate, each ledger entry is one node, so a declared id resolves to the entry that carries that package. A dependency cycle never stops a hydrate: its members keep source order and an error names them.
27+
28+
**BREAKING** (an install-acceptance narrowing of the unreleased ADR-0141 D2 check) at `POST /api/v1/marketplace/install-local`. A multi-package artifact's own wrapper id no longer counts as delivered unless one of its packages carries it. A module declaring the wrapper id is judged like any other entry, because it would be `absent` on its first standalone install. `DeclaredDependencyCheck.deliveredIds` is removed with it. Also, an undecided range union whose installed version is readable is now reported `found: 'unreadable-range'` instead of `'unreadable-version'`; for example, `<5 || latest` against `7.0.0`.
29+
30+
**What changes for you.**
31+
32+
- A module that extends an app and uses its namespace declares the app in `manifest.dependencies`, and then installs beside it as its own artifact. Two modules of one app need not declare each other.
33+
- Every package of the app's own artifact that uses the namespace should declare the app too.
34+
- New exports: `@objectstack/core` exports `orderHydrationBatch` and `declaredDependencyIds`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/plugin-chatbot': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(plugin-chatbot,app-shell): a whole-app build approved from the proposed plan draws its result card, live and after a reload
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Approving a proposed plan sends the approval sentence, and the runtime replays the approved `apply_blueprint` under a `replay_` tool-call id. The chat read every replay result as the verdict of a confirm card and dropped its draft review, and a replay streams no build-progress frames, so the build's own envelope (`drafted[]`, `packageId`, `checklist`, `nextSteps`) reached the chat and was drawn nowhere: no result card, no Open app, no checklist, no next steps.
12+
13+
- **`@object-ui/plugin-chatbot`:** a replayed whole-app build that drafted keeps its draft review and draws the same result card as a model-driven build. A replayed build in an auto-publish environment (`status: 'published'`) draws the card and its next steps from that envelope; so does a published build after a reload. A replay whose publish rolled back, and a replayed incremental edit, are unchanged.
14+
- **`@object-ui/app-shell`:** the AI chat page's hydration keeps the same draft review, so a reloaded thread binds to the package the replay built, and the conversation cache keeps that build's draft envelope instead of the bare replay verdict.
15+
16+
What to do: nothing.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime)!: the action door and the flow trigger door refuse a body or a `params` that is not a JSON object, instead of running with an empty bag
6+
7+
`POST /api/v1/actions/:object/:action` read its body, and then `params`, only when each was a truthy object. A string such as `{"params":"oops"}` became an empty params bag and the action ran anyway, answering `200` and starting a run the same as `{"params":{}}`; an array `params` was handed on as the bag itself; and a JSON string or array body ran the action with an empty bag too.
8+
9+
`POST /api/v1/automation/:name/trigger`, its legacy spelling `POST /api/v1/automation/trigger/:name`, and every declared `type: 'flow'` API endpoint read the request body only when it was a truthy object. A JSON string body dispatched the flow with an empty bag, so it failed later with a misleading `400 FLOW_FAILED` "Unknown variable" or ran doing the wrong thing, and an array body forwarded its indices as params.
10+
11+
Both now answer `400 VALIDATION_FAILED` with one `fields[]` entry, `{ field: '(body)', code: 'invalid_type' }` for a body and `{ field: 'params', code: 'invalid_type' }` for a `params`, whose message names the type received. The refusal comes before anything is dispatched, so no run starts and no run row is written. It is the refusal the resume door already gives a non-object body, and the three doors now ask the question through one check, so their answers have the same shape; the resume door's `fields[]` message now also names the type received.
12+
13+
Unchanged: an object `params` (an empty one included) reaches the action as before, and an action call with no body, a JSON `null` body or no `params` key still runs with an empty bag. A trigger with no body, or a JSON `null` body, still runs with an empty bag. An unknown action or flow still answers `404`, and a caller the action or flow refuses still answers `403`, before the body is judged.
14+
15+
Clause-②: no (narrowing) — the action and trigger doors refuse a body that is a string, number, boolean or array, and the action door refuses a `params` that is a string, number, boolean, array or `null`. Nothing is widened.
16+
17+
Metadata-protocol: none (an HTTP request body's handling at the action and trigger doors; nothing a metadata author writes changes)
18+
19+
<!-- adr-0087: not-required (no-migration-prescription) No metadata 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, so there is nothing for `objectstack migrate meta` to rewrite. What changes is how the action and flow trigger doors handle a request whose body or `params` is not a JSON object: it is refused instead of dropped. The remedy is a request edit (send `params` or the body as an object, or leave it out), never a rewrite of a package source or a stored row. 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). -->
20+
21+
**BREAKING** for a caller that sent the action door's body or `params`, or the trigger door's body, as something other than an object and relied on the call running anyway, shipped as `patch` because the metadata protocol does not move.
22+
23+
- **Before.** A non-object body or `params` on the action door, or a non-object trigger body, was dropped (an array trigger body was forwarded as index-keyed params), and the action or flow was dispatched.
24+
- **Now.** The request is refused with `400 VALIDATION_FAILED` naming `params` or `(body)`, and nothing runs.
25+
- **Fix.** Send `params` as an object of input names to values, for example `{"params":{"note":"x"}}`, send the action body as an object (`{"recordId":"…","params":{…}}`), and send a trigger body as an object (`{"params":{"ownerId":"u1"}}` or a flat `{"ownerId":"u1"}`), or leave any of them out.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a `notify` node naming `email` and another channel mails an address no user holds and no longer writes an inbox row addressed to it
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An email address written out in `recipients` is the author's constant: on a node whose `channels` name `email` it is mailed as written. The inbox delivers to a user, keying its row by the recipient as a user id, and so does every channel but `email`. So on a node whose `channels` name `email` and another channel, such as `['inbox', 'email']`, an address no user holds now goes to the email channel alone.
11+
12+
- **Fixed.** Before, the address went to every channel as it stood: the email channel mailed it, and the inbox also wrote a `sys_inbox_message` row whose `user_id` was the address, which no inbox reads. Now the node emits that address apart, on the `email` channel only, and the inbox writes no row for it. `user:ADDRESS` is handled the same way, whoever holds the address, because `user:` hands the address on as the user id with no lookup. An address whose holder cannot be looked up (no data engine, a lookup that fails) is mailed alone too.
13+
- **Unchanged.** The node succeeds as before. An address a user holds, written bare, still reaches that user's inbox and is mailed. A value expression that evaluates to an address on a node naming `email` still resolves to the user who holds it, or fails the node pointing to `{ emailField }`. A node that does not name `email` still fails on an address no user holds.
14+
- **What a run records.** When a node's recipients mix users and such addresses, the node writes two `sys_notification` events: one for the users on the node's `channels`, which the node's `notificationId` output names, and one for the addresses on `email` alone. The `delivered`, `enqueued` and `failed` outputs add up both.
15+
- **Who moves.** Nobody needs to change a flow. A report or audit over `sys_inbox_message` no longer sees rows whose `user_id` is an address.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the flow inspector's list field shows a stored string as one entry instead of an empty list
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A notify node's `channels` may be a single string (`channels: 'email'`) or a list; the spec accepts both and the runtime reads the string as a one-item list. Studio's flow inspector read any non-list value as empty, so the field showed no channels and adding one saved a list without `email`. A stored non-blank string now reads as a one-entry list (trimmed, as the runtime does). An edit saves the list form (`['email', 'sms']`); a field left untouched keeps the stored string.

‎README.md‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,18 @@ git clone https://github.com/objectstack-ai/objectstack.git
248248
cd objectstack
249249
pnpm install # Node 22+, pnpm 10 (corepack enable)
250250
pnpm build # build all packages
251-
pnpm objectui:build # build the Console SPA (not part of pnpm build)
251+
pnpm console:build # put the Console SPA in packages/console/dist
252252
pnpm dev # showcase example: REST + Console on :3000
253253
```
254254

255-
`pnpm objectui:build` builds [objectui](https://github.com/objectstack-ai/objectui)
256-
at the commit pinned in `.objectui-sha` into `packages/console/dist`
257-
(gitignored). It builds from a `../objectui` checkout if one sits next to this
258-
repo, otherwise from a shallow clone of objectui into `.cache/`; either way it
259-
installs objectui's dependencies, so it needs network. Skip it and `pnpm dev`
260-
still serves the REST API, without the Console. Rerun it when `.objectui-sha`
261-
moves.
255+
`pnpm console:build` builds the Console from its source in this workspace
256+
(`ui/apps/console`, against this tree's own spec and client) and copies the
257+
result into `packages/console/dist` (gitignored), which is where `pnpm dev`
258+
serves it from. Nothing is cloned or pinned. `pnpm build` builds the Console
259+
too but does not copy it, so after `pnpm build` this step is a copy. Skip it
260+
and `pnpm dev` still serves the REST API, without the Console. Rerun it after
261+
pulling changes under `ui/`: `packages/console/dist` is never refreshed on its
262+
own.
262263

263264
A first run is slow, not stuck. Measured once on a fresh clone of `main`
264265
(4-vCPU container, Node 22.22, pnpm 10.31):
@@ -267,7 +268,7 @@ A first run is slow, not stuck. Measured once on a fresh clone of `main`
267268
|:---|---:|:---|
268269
| `pnpm install` | 16 s | |
269270
| `pnpm build` | 6 m 54 s | 72 turbo tasks |
270-
| `pnpm objectui:build` | 10 m 36 s | clones objectui and builds the Console |
271+
| `pnpm console:build` | 12 m 16 s | 72 turbo tasks: the Console and what it depends on, run before any `pnpm build` (`--concurrency=2`); a cache hit plus a copy, about 1 s, once `pnpm build` has run |
271272
| `pnpm test` | 54 m 03 s | the full suite: 135 turbo tasks |
272273

273274
For your own change, test what it reaches instead of the full suite:

0 commit comments

Comments
 (0)