Skip to content

Commit 05cb95f

Browse files
authored
Merge branch 'main' into claude/issue-13798-self-test-handshake-b2
2 parents 61850ab + 431979e commit 05cb95f

182 files changed

Lines changed: 12157 additions & 966 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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): the mail transport, brand and locale bindings no longer depend on `registerRoutes`
6+
7+
`AuthPlugin` bound five things to the live kernel on `kernel:ready` — the
8+
outbound mail transport (`setEmailService`), the SMS transport
9+
(`setSmsService`), the deployment email locale (`setDefaultEmailLocale`), the
10+
brand name (`setAppName`) and the SMS locale (`setDefaultSmsLocale`) — from
11+
inside the same hook that mounts `/api/v1/auth/*`, and that hook was gated on
12+
`registerRoutes`.
13+
14+
`registerRoutes` answers a transport-mounting question: should this plugin put
15+
its own routes on the kernel's `http-server`. The bindings are service
16+
composition, and they are true of an embedding regardless of who serves the
17+
routes. So an embedding that serves auth routes itself — the whole point of
18+
`registerRoutes: false` — came up with no mail transport, no locale on either
19+
channel and no brand binding. Silently: the `logger.info` lines that report the
20+
wiring were inside the same skipped block, and the `localization` settings
21+
namespace was not even read. One visible consequence was that the workspace
22+
language could not reach auth mail on such a host at all, and
23+
`/api/v1/auth/config` answered `requireEmailVerification: false` because
24+
`resolveRequireEmailVerification()` saw no transport.
25+
26+
The composition block now registers as its own unconditional
27+
`ctx.hook('kernel:ready', …)` — the shape the sibling diagnosis and dev-seed
28+
hooks in this plugin already use — placed before the route hook so a routing
29+
host keeps the ordering the single combined hook gave it.
30+
31+
Route registration itself stays gated: a `registerRoutes: false` kernel still
32+
mounts no auth routes.
33+
34+
**Behaviour change for `registerRoutes: false` embeddings.** They now resolve
35+
the `email`, `sms`, `i18n` and `settings` services at `kernel:ready`, apply
36+
`branding.workspace_name` and `localization.locale` (subscribing to both), seed
37+
the built-in auth SMS templates when phone sign-in is enabled, and emit the
38+
four wiring `info` lines. Hosts that had compensated by wiring these by hand
39+
should expect the plugin's own binding to run as well; both paths are
40+
idempotent setters, and an explicit workspace setting keeps outranking a
41+
manifest default exactly as it does on a routing host. Nothing changes for a
42+
host that leaves `registerRoutes` at its default.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
refactor(cli): spell the action-dedup object-less key as `GLOBAL_ACTION_OBJECT_KEY` (#14669)
6+
7+
`os lint` dedups action declarations on the engine's composite registration key
8+
(`<objectName>:<name>`), and the object half of that key terminated on a bare
9+
`'global'` string literal in `lintConfig`'s `PREFIXED_TYPES` table. The engine's
10+
own writers stopped spelling the literal: PR #14667 converged
11+
`ObjectQLPlugin.actionObjectKey` onto the shared `GLOBAL_ACTION_OBJECT_KEY`
12+
constant for exactly this reason — a copy that agrees by value today is the one
13+
that parts from the writer in silence the day the constant moves, with no test
14+
in the repo able to see it. This reader now imports the constant from
15+
`@objectstack/objectql`, which `@objectstack/cli` already depends on.
16+
17+
**No behaviour moves.** `GLOBAL_ACTION_OBJECT_KEY` is `'global'`, so every key
18+
this table builds is byte-identical to the one it built before; the #5510 dedup
19+
suite (`lint-namespace-prefix.test.ts`, 15 declarations over 5 objects) passes
20+
unchanged. Only `objectName` is read, exactly as before — the `object`/`entity`
21+
aliases are still rejected upstream by `ActionSchema`'s strict shape and are
22+
deliberately not admitted here.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`composeStacks` now refuses two input stacks whose action declarations resolve to one scope-qualified runtime key — **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes.
6+
7+
**The refused shape:** two (or more) input stacks that each declare an action resolving to one runtime key — `objectName:name`, or `global:name` for an object-less action. The canonical case is two packages, each legal on its own, each declaring a global action named `shared_refresh`: `composeStacks([a, b])` used to accept the pair and emit `["global:shared_refresh", "global:shared_refresh"]`, and the runtime — which registers and dispatches every action under that one exact-string key — collapsed both handlers to one registration: whichever registered second won, and the other package's action stayed a live, declared, permission-gated button whose handler was unreachable. `defineStack` refuses exactly this collision within one stack (#14686); composition was the second door in the same file that let it through.
8+
9+
The refusal carries `composeStacks`' conflict prefix and `defineStack`'s envelope shape — `composeStacks conflict: cross-stack action key collision (N issue(s)):`, one `` line per colliding key — and every line names the key, **both source stacks by manifest id** (`'com.example.a' (stack #0)`; a manifest-less input is named by position), and where each declaration sits (`stack.actions[i]`, or `objects['OBJECT'].actions[j]` for an embedded one). The fix is the one the message names: rename one of the colliding actions within its scope, bind one of them to a different object, or remove the duplicate from one of the stacks. ⛔ There is no `actionConflict` option and none is coming: `composeStacks` does not pick a winner for actions (maintainer ruling, 2026-09-03).
10+
11+
What the check judges is what composition **carries**: standalone actions from every stack (they concatenate), and each composed object's embedded actions attributed to the one stack whose object `objectConflict` handed the `actions` array to. So an embedded action that `'override'` or `'merge'` did not carry into the composed object cannot collide — that loss is the object strategy's own semantics — while a standalone action bound to an object from one stack does collide with an embedded action of the same name the composed object carries from another. Only a key declared by two or more **distinct** stacks is reported: a key an input repeats within itself is `defineStack`'s door (`strict: false` opts out there by choice), and an input built by `defineStack` legitimately carries each bound standalone action twice — as the copy the build appended to its object — which is never a collision with itself.
12+
13+
Deliberately unchanged, as in `defineStack`: one global and one object-bound action may share a name across stacks (two keys), and one name bound to two different objects is two keys. The shipped composer, `examples/app-multi-package`, declares no colliding key and composes unchanged; no `composeStacks` caller exists in objectui or hotcrm.
14+
15+
**Migration.** A composition refused by the new check must resolve the collision in one of the two packages — rename the action within its scope, bind it to a different object, or drop the duplicate. Which package keeps the name is an authoring decision the metadata cannot make for you.
16+
17+
<!-- adr-0087: not-required (no-migration-prescription) a validity narrowing over existing declarations: no key is removed, renamed or re-shaped and no export moves, so the ledger has no rewrite to carry; the refusal itself names the remedy at the composition site, and choosing which of two packages keeps the action name is an authoring decision no migration entry can perform on an upgrader's behalf. -->
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
"@objectstack/lint": patch
4+
"@objectstack/mcp": patch
5+
---
6+
7+
fix(ai): author the CANONICAL agent id everywhere the platform teaches one — Studio's pin, the MCP prompt example, and the lint's value roster (#14461)
8+
9+
`skills/objectstack-ai` tells authors that `data_chat` and `metadata_assistant`
10+
"are **not** vocabulary — always write `ask` / `build`". The platform then
11+
taught the opposite from every live example it ships. Nothing was broken at
12+
runtime; what was wrong is what an author copies.
13+
14+
**Studio's pin.** `studio.app.ts` was the repo's ONLY `app.defaultAgent` usage,
15+
and it spelled the alias:
16+
17+
```
18+
- defaultAgent: 'metadata_assistant',
19+
+ defaultAgent: 'build',
20+
```
21+
22+
The triage card left this undecidable — if the cloud plugin registered the
23+
agent under the legacy id, re-pinning would be a behaviour change in a
24+
consumer this repo cannot see. Measured instead of assumed, at `cloud`
25+
`main@3856fbf7`: `service-ai-studio/src/agents/metadata-assistant-agent.ts:12,40`
26+
ships the record as `name: BUILD_AGENT_NAME` = `'build'`, and `plugin.ts:58`
27+
registers `metadata_assistant` as a **one-way, resolution-only** legacy alias.
28+
The canonical id *is* `build`; the old pin reached it by detour.
29+
30+
Nor is the re-pin cosmetic. Alias resolution depends on an in-memory
31+
`registerAgentAlias` call having run at plugin init, and cloud carries two
32+
defensive docblocks about that registration silently no-op'ing for real under
33+
bundle load ordering (`service-ai-studio/src/plugin.ts:44-57`,
34+
`service-ai/src/agent-runtime.ts:30-41` — "a missed alias must never hide a
35+
real platform agent like `build`"). The canonical id never touches the alias
36+
table, so this drops a load-order dependency from the platform's own flagship
37+
authoring surface. On the UI side nothing moves: `objectui`'s
38+
`AGENT_ALIAS_GROUPS` is bidirectional and canonical-first, and
39+
`SURFACE_DEFAULT['studio-build']` was already `'build'`.
40+
41+
**The MCP prompt example.** `mcp-server-runtime.ts`'s `agent_prompt` argument
42+
described itself as `'Name of the agent to load (e.g. "data_chat",
43+
"metadata_assistant")'` — two retired aliases, neither canonical id present.
44+
That string is served to every MCP client asking what to pass, so the one
45+
surface that suggests a spelling to an LLM suggested the two the catalogue
46+
forbids. Now `(e.g. "ask", "build")`.
47+
48+
**The lint's value roster.** `validate-ai-agent-authoring`'s `defaultAgent`
49+
**value** limb reused the four-name `PLATFORM_AGENT_NAMES` set, so it
50+
deliberately passed `metadata_assistant` — the gate that exists to make
51+
authoring mistakes loud waved through the exact spelling the catalogue bans,
52+
which is the silent-tolerance shape ADR-0078 exists to close, committed by the
53+
gate itself. The two limbs now read different tables, because they ask
54+
different questions:
55+
56+
- **declaration limb** — unchanged, still all four names. Declaring
57+
`metadata_assistant` shadows the `build` record through the alias exactly as
58+
declaring `build` does.
59+
- **value limb** — canonical `ask` / `build` only. A legacy alias gets its own
60+
rule id `default-agent-legacy-alias` (exported) and its own wording, because
61+
an alias **resolves** (the app gets the agent it meant — a spelling defect)
62+
while an unknown name does **not** (the pin is inert). Describing the alias
63+
as "no effect" would send an author hunting a bug that is not there.
64+
65+
Both of the #6041 ruling's operative decisions are kept intact: still
66+
`warning` tier, still no Zod enum narrowing. `defaultAgent: 'metadata_assistant'`
67+
keeps parsing, building, and resolving — the only change is that authoring it
68+
now says so.
69+
70+
Not breaking: nothing an author can write was removed, and both aliases stay
71+
resolvable for old bookmarks and persisted `agent_id`s, which is the only job
72+
ADR-0063 §2 ever gave them.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
Fix `os explain flow`, whose example taught a flow shape the spec rejects and an assignment value nothing resolves.
6+
7+
`os explain` is an authoring aid whose whole audience is authors — increasingly AI authors — writing their first flow, and its catalog is hand-maintained rather than derived from `FlowSchema`. The flow entry had drifted until the sample it printed could not be pasted into a working app:
8+
9+
- `steps` and `trigger` are strict-object **aliases** on `FlowSchema` (for `nodes` and `type`), so authoring either is a loud parse error rather than a working flow. A record-change flow binds its object on the START node's `config` (`{ objectName, triggerType }`), not at the flow top level.
10+
- A node's per-type data lives under `config`, so the sample's top-level `field`/`value` pair were undeclared keys on a `.strict()` node schema, and the required `id` / `label` were absent. `edges` is required, and the sample declared no graph at all.
11+
- The value `'$currentUser'` was a `$`-prefixed sentinel no resolver in the platform recognises. Flow values interpolate with **single braces**, and the acting user is `{$User.Id}` — the filter surface's `{current_user_id}` is a different dialect that does not carry over, because assignment and `fields` values go through `interpolate`, not `interpolateFilter`.
12+
- An `assignment` node sets a flow **variable**, not a record field, so "auto-assign on create" is an `update_record` node. The old sample would not have written `assigned_to` even with a resolving token.
13+
14+
The entry's field list now matches `FlowSchema` (`nodes` / `edges` / the full five-value `type` enum / `status` / `runAs`), and the example is pinned by a test that parses it against `FlowSchema` — the one guard that cannot drift alongside the catalog it checks.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): a top-level flow `trigger` / `triggerType` is now sent to the START node's `config`, not to a `type` rename
6+
7+
`FlowSchema`'s alias table pointed both keys at `type`, so a flow carrying a
8+
top-level trigger block was refused with the rename `trigger``type`.
9+
That rename cannot be taken: `type` is the flow KIND
10+
(`autolaunched` | `record_change` | `schedule` | `screen` | `api`), so an author
11+
who followed the advice landed on
12+
`Invalid option: expected one of "autolaunched"|…` one round later, with the
13+
trigger binding still nowhere — and a `.strict()` refusal carries exactly one
14+
actionable sentence.
15+
16+
The trigger does not move to `type`. It binds on the START node's `config`, as
17+
`{ objectName, triggerType, condition }` with a `record-*` token such as
18+
`record-after-create` — the shape the automation engine and the authoring-time
19+
`resolveFlowTriggerKind` both read. Both keys are `guidance` entries now, beside
20+
the `object` / `objectName` / `schedule` prescriptions that already name that
21+
config, so the rejection says where the binding really lives instead of
22+
prescribing a name:
23+
24+
```
25+
Unrecognized key(s) on this flow: `trigger`.
26+
• `trigger` is not a Flow field — a record-change flow binds its trigger on
27+
the START node's `config` (`{ objectName, triggerType, condition }`, where
28+
`triggerType` is a `record-*` token such as `record-after-create`), not at
29+
the flow top level; the flow-level `type` names the flow kind
30+
(`record_change`), not the binding.
31+
```
32+
33+
No accept/reject behaviour changes: a top-level `trigger` / `triggerType` was
34+
refused before and is refused now, and `FlowSchema`'s accepted keys and its
35+
`type` enum are untouched — only the prescription the refusal carries. One
36+
measured consequence of dropping the alias row: the guidance channel matches the
37+
exact authored spelling (case folding is the rename channel's job), so a
38+
non-canonical spelling such as `triggertype` now gets the bare rejection rather
39+
than the rename it cannot take.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate` now has an answer for every field type, instead of silently guessing
6+
7+
Three hand-kept vocabularies in `generate.ts` decide what `os generate types`
8+
and `os generate migration` emit for a field: the TypeScript type, the SQL
9+
column type, and the knex builder call. None of them was ever checked against
10+
the `FieldType` enum they describe, and measured against the 49 members on
11+
`main`, **21 real members had no entry in either lookup table and 24 had no arm
12+
in the migration switch**.
13+
14+
An unmapped member did not fail — it fell to the default. So a `secret` field
15+
scaffolded as TypeScript `unknown` and a `TEXT` column, a `location` as
16+
`unknown` and `TEXT`, and `address` / `composite` / `repeater` / `record` — all
17+
four stored as JSON on the parent row — as scalar `TEXT` columns. The output
18+
looked plausible and nothing said otherwise, which is what made this worth
19+
fixing rather than tidying.
20+
21+
All 49 members now have an entry in all three, and the values are read off the
22+
platform rather than invented: the spec's ADR-0104 D1 value classes
23+
(`STRING_VALUE_TYPES`, `NUMERIC_VALUE_TYPES`, `STRUCTURED_JSON_TYPES`, …) decide
24+
the class, and `driver-sql`'s own DDL emitter — which creates the real columns —
25+
decides the shape. `location` becomes a JSON column, not a `POINT`: that is what
26+
the driver does, `POINT` is not portable to SQLite, and the spec's own value
27+
contract for it is `{lat, lng, altitude?, accuracy?}`. `location` and `address`
28+
now emit the spec's exported `Data.LocationValue` / `Data.AddressValue` types,
29+
so the generated interface cannot drift from the value contract.
30+
31+
The gap can no longer reopen quietly. Both lookup tables are
32+
`satisfies Record<FieldType, string>`, so a field type added to the spec is a
33+
named compile error here; the switch — whose scrutinee is a plain string off an
34+
unvalidated config and so cannot carry one — is held by
35+
`generate-field-type-vocabulary.pin.test.ts`, which walks the real enum and
36+
names any member left unmapped.
37+
38+
The runtime fallbacks (`|| 'unknown'`, `|| 'TEXT'`, `default:`) are unchanged
39+
and still reachable: they answer a `type` string that is not a field type at
40+
all, which the unvalidated authoring door can still deliver.

0 commit comments

Comments
 (0)