Skip to content

Commit f528ddb

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14428-driver-update-missing-id
2 parents c2400c5 + 7317cf2 commit f528ddb

60 files changed

Lines changed: 3335 additions & 396 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
Report the zero-account boot dead end at `kernel:ready` (#14353)
6+
7+
A deployment holding human `sys_user` rows and zero `sys_account` rows cannot
8+
be recovered from inside, and until now it booted silently. Nobody can sign in;
9+
the first-account bootstrap carve-out counts humans, and humans exist, so it
10+
does not open; the default `invite_only` audience posture refuses
11+
self-registration; and no administrator exists who could send an invitation.
12+
The only symptom was a 401 on credentials nobody holds.
13+
14+
That state is now reported at `kernel:ready` at `error` level, under the name
15+
`no_sign_in_account_at_boot`, naming both the consequence (the deployment will
16+
keep looking healthy and cannot be recovered from inside) and the remedy
17+
(provision an account out of band, or open the audience posture).
18+
19+
⛔ No admission semantics change. Whether the carve-out should count humans or
20+
logins was ruled on 2026-09-02 (option A — the door does not move); this only
21+
reports.
22+
23+
The check extends the existing `kernel:ready` walled-owner reporter rather than
24+
opening a parallel one: it shares that hook, and the bounded human-population
25+
page is read ONCE per boot and handed to `probeWalledOwnerAccountState`, so no
26+
deployment pages `sys_user` twice. At most one report is emitted per boot — a
27+
deployment matching both shapes gets this error, and the walled-owner warning
28+
is suppressed rather than stacked on top of it.
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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): wire `packages/cli`'s test layer into `check:test-typecheck`, so its 115 test files are type-checked at all (#14710)
6+
7+
`packages/cli/tsconfig.json` declares `include: ["src"]` and no `exclude` at
8+
all, and the package's `typecheck` script was a bare `tsc --noEmit` against that
9+
very config — so the 115 test files in the sibling `test/` tree were read by no
10+
tsc program anywhere. This is the shape AGENTS.md already forbids, reached by
11+
the OTHER spelling: not an `exclude` naming the test globs, but an `include`
12+
that never reaches them.
13+
14+
Measured at `5a5336b399` with the workspace closure built first, rather than
15+
read off the config: `tsc --noEmit --listFiles -p tsconfig.json` puts 1009 files
16+
in the program and **0** of the 115 among them, while 119 of 119 non-test
17+
`src/**` files and all 121 `src/**` test files ARE there — so the zero is the
18+
`include` line, not a probe that sees nothing. The directional control is
19+
`packages/drivers/driver-memory`, whose tsconfig carries no test exclusion: the
20+
same probe puts 40 of its 40 test files in the program. Under the new
21+
`tsconfig.test.json` the count is **115 of 115**, plus the three package-root
22+
harness modules (`vitest.config.ts`, `vitest-tiers.ts`,
23+
`vitest-tiers.fixtures.ts`) and `test/helpers/serve-process.ts`.
24+
25+
Onboarded by *wiring* to the shared mechanism (`scripts/check-test-typecheck.mts`)
26+
the way `objectql`, `rest`, `lint`, the fourteen `packages/plugins/**` and
27+
`runtime` are wired, never by copying it: a sibling `tsconfig.test.json`
28+
matching vitest's real module semantics (`module: esnext`,
29+
`moduleResolution: bundler`), named by `typecheck` via
30+
`check:test-typecheck --project`. Strictness is untouched and inherited; not one
31+
`any` and not one `@ts-expect-error` was added to any test file to open the
32+
gate. `rootDir` IS widened to `../..`, the way `packages/client`'s test config
33+
already does it — this package's tests sit outside the build config's
34+
`rootDir: "src"`, and three of them import fixtures from
35+
`examples/app-showcase/src/**`.
36+
37+
**Seeded, not repaired, per this card's triage ruling.** The layer reports 28
38+
errors across 3 files and they are recorded EXACT and shrink-only in the new
39+
`test-typecheck-debt.json`. Every one is pre-existing: no test file is edited
40+
here. The other 112 files carry no entry, so any error they gain is red on
41+
arrival.
42+
43+
This is a CONVERSION rather than a new debt-opening decision. The same
44+
population under the build config's inherited NodeNext reports 144 — exactly the
45+
number `scripts/check-type-check-coverage.mjs` already held for this package in
46+
its per-PACKAGE `TEST_DEBT` ledger, class for class — and that entry graduates
47+
here, as the pairing forces. The 144 → 28 step is attributed in both directions
48+
with no remainder: −120 config-tier diagnostics that dissolve under vitest's
49+
module semantics (TS2835 ×56, the TS7006 ×59 cascading above them, TS2307 ×3,
50+
TS18046 ×2) and +4 that collapsing the cascade exposed (TS18048 ×4 in
51+
`test/i18n-extract-action-description.test.ts`, previously masked by an `any`
52+
from two unresolved imports). The 24 TS2339 survive unchanged, file for file.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
refactor(runtime): spell the object-less action key as `GLOBAL_ACTION_OBJECT_KEY` in `action-execution.ts` (#14678)
6+
7+
`GLOBAL_ACTION_OBJECT_KEY` exists so the object-less action-registration key is
8+
written once. #14422 converged the owner-key LADDER and the ObjectQL plugin's
9+
copy of it; three bare `'global'` spellings elsewhere in
10+
`packages/runtime/src/action-execution.ts` were never in that card's path,
11+
because the runtime fence it built was a re-export plus a delegating alias.
12+
This converges those three. The constant was already imported in the file.
13+
14+
No behaviour moves — the constant is `'global'`, so every site is equal in
15+
value before and after. That equality is the entire defect: it is what made the
16+
three invisible to every test in the repo, and what would have let them part
17+
from the constant in silence the day its value changes.
18+
19+
- `seedFlowActionParams` — a live comparison (`objectName !== 'global'`) that
20+
decides whether an object-derived `<object>Id` param key is seeded. The one
21+
site where a drifted literal would change what an action body receives.
22+
- `enforceActionParams` — the warn-once dedup key, which is also interpolated
23+
into the operator-facing `[action-params] <key>: …` line. Converged rather
24+
than left: the argument for a literal here is that a log key must never fail
25+
to render, and that argument does not survive contact with the fact that
26+
`GLOBAL_ACTION_OBJECT_KEY` is a module-scope `const string` already imported
27+
into this file — it cannot fail to render either. What a drift there would
28+
actually cost is an operator grepping logs by the key the engine now uses and
29+
silently missing these lines.
30+
- `collectActionDeclarations`'s docblock, which carried a second defect
31+
independent of the literal: it called the key "the `'global'` wildcard",
32+
contradicting `action-governance.ts` ("an exact-string `Map` lookup with no
33+
wildcard semantics"). It is now the phrasing the sibling docblock 48 lines
34+
below it already used — "the object-less `GLOBAL_ACTION_OBJECT_KEY`" — so the
35+
correction is copied from the file's own converged prose rather than invented.
36+
37+
`patch`, not `skip-changeset`: `packages/runtime` publishes `dist`, which is
38+
built from this source, so the emitted bytes move even though the behaviour
39+
does not. Nothing reaches the published entry — `action-execution.ts` is not
40+
re-exported from `packages/runtime/src/index.ts` and no export, signature or
41+
type changed here — which is what keeps it below `minor`.
42+
43+
The docblock that promised the lockstep is joined by a weld that enforces it:
44+
`action-owner-key-single-source.test.ts` gains a half C that reads
45+
`action-execution.ts` and fails if any quote spelling of the key is written out
46+
by hand again. The forbidden spelling is DERIVED from the constant rather than
47+
hard-coded, so the guard is not itself a fourth copy of the literal it forbids.
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): the published `explain` payload contract now names BOTH zero-rows sentinels
6+
7+
`ExplainDecision.readFilter` and `ExplainRecordAttribution.rowFilter` are the
8+
machine artifact behind the explain prose, and their published description
9+
enumerated the zero-rows vocabulary as a closed two-item list: `null` =
10+
unrestricted, `{ id: '__deny_all__' }` = zero rows.
11+
12+
That enumeration had grown incomplete. A fail-closed RLS denial — the "no
13+
active organization" path, which composes plugin-security's `RLS_DENY_FILTER`
14+
and is guaranteed to return zero rows — is reported with verdict `denies` and
15+
`allowed: false`, while the payload keeps reporting the predicate that was
16+
ACTUALLY composed: an `id` equality against `__rls_deny__` plus a colon and a
17+
UUID-shaped suffix. So a reader of the contract met a zero-rows shape the
18+
contract did not name.
19+
20+
Both fields now name both shapes, say that the RLS denial is published as
21+
composed (and can therefore ride inside an `$and` composite on `readFilter`),
22+
and say which fields are the DECISION — `allowed` and the `rls` layer's
23+
`verdict` for `readFilter`; `outcome` / `matchesRecord` and the layer's
24+
`verdict` for `rowFilter` — so a consumer that pattern-matches the payload
25+
alone to detect "zero rows" is told it must match both.
26+
27+
`readFilter` carried its enumeration in a JSDoc block only, which no generator
28+
reads: its published description cell and its JSON Schema `description` were
29+
both EMPTY. It now carries a `.describe()`, so the reference page and the
30+
emitted JSON Schema publish the vocabulary instead of nothing.
31+
32+
Text only. `readFilter` / `rowFilter` remain `z.unknown()`, no accepted value
33+
changes, no emitted payload changes, and the two sentinels are not unified —
34+
the sentinel vocabulary itself is a separate, deployment-facing decision.
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.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the sandbox hook write-back carries the keys the body wrote, not every key it could see
6+
7+
A sandboxed `before*` hook body's mutations were written back onto the engine's
8+
payload with `Object.assign(target, mutatedInput)`, where `mutatedInput` is the
9+
whole post-run `ctx.input` — every key the body could see, touched or not. That
10+
target is the engine's flat-input Proxy, and every assignment through it is
11+
recorded by the hook-write provenance recorder. So the write-back was reporting
12+
that a body which touched nothing had written every payload key.
13+
14+
The per-row divergence refusal that ends "one hook-mutated payload applied to
15+
every matched row" on a `multi: true` update reads exactly that recording: per
16+
row, the key set the hook chain assigned, refusing the batch when two rows
17+
disagree. All rows share ONE payload, so the noise was order-dependent — with a
18+
transition stamp bound to `beforeUpdate`, one open row and one already-completed
19+
row:
20+
21+
- already-done row dispatched first: the windows differ, the batch is refused;
22+
- open row dispatched first: the already-done row inherits `completed_at` from
23+
the transitioning row's write onto the shared payload, the blanket write-back
24+
re-asserts it as that row's own write, the windows match — and the refusal
25+
abstains, moving a `completed_at` on a record that never transitioned.
26+
27+
The refusal was therefore true for in-process handlers and, in one of two driver
28+
row orders, silently untrue for shipped hook bodies. The QuickJS runner now arms
29+
a write recorder on `ctx.input` for hook bodies — the same recorder shape
30+
`ctx.record` has used since the discarded-record-write report — and the
31+
write-back re-asserts only the keys the body assigned, defined or deleted.
32+
33+
Nothing else about the channel moves:
34+
35+
- deletion still propagates, unchanged. It was never expressed by the merge:
36+
the write-back reads it from the entry snapshot as absence-from-the-dump,
37+
before both merges, and a key the body never touched is present in the dump
38+
and so is never deleted.
39+
- a write made THROUGH a value read from the input (`ctx.input.meta.x = 1`)
40+
trips no trap on `ctx.input` itself, so it is carried from the dump instead:
41+
an object-valued entry key whose dumped value no longer matches the entry
42+
snapshot was written through. Primitives need no such leg — a primitive
43+
cannot be mutated in place.
44+
- when the recorder cannot speak — an older runner, a read that failed, a body
45+
that replaced `ctx.input` with a non-object — the write-back falls back to
46+
the full assign it did before. Narrowing on a key set that is not trustworthy
47+
would silently drop a write the body really made. An empty key set is a
48+
different answer from an absent one and does narrow.
49+
50+
Hook bodies only; the action path has no input write-back to inform.

0 commit comments

Comments
 (0)