Skip to content

Commit 236b66e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14587-zh-cn-dashboard-sections-layout
2 parents e5be136 + 450e73b commit 236b66e

39 files changed

Lines changed: 2688 additions & 137 deletions
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: 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.

content/docs/data-modeling/validation-rules.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ section above). See the
539539
| `lookup` | `reference` | Foreign key integrity |
540540
| `user` || Lookup specialized to `sys_user`; `multiple: true` stores an id array |
541541
| `master_detail` | `reference` | Cascade delete, ownership |
542-
| `tree` | `reference` | Self-referencing; no automatic cycle check |
542+
| `tree` | | Self-referencing; no automatic cycle check |
543543
| `image` || Common image MIME types; `multiple` for many |
544544
| `file` || Any file type; `multiple` for many (no field-level upload config) |
545545
| `avatar` || Single image, typically square |

content/docs/deployment/troubleshooting.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ The custom error map provides "Did you mean?" suggestions for common typos.
7575

7676
### "Required property missing: reference"
7777

78-
**Symptom:** A `lookup`, `master_detail`, or `tree` field fails validation.
78+
**Symptom:** A `lookup` or `master_detail` field fails validation.
7979

8080
**Cause:** Relational fields require a `reference` property pointing to the target object.
8181

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ that silently does not happen.
9797
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3857` |
9898
| 9 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `anonymous-deny.ts:154` |
9999
| 10 | Permission-set projection middleware skipped | plugin-security | Lose: projection of permission-set-derived columns | `permission-set-projection.ts:1015` |
100-
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1380` |
100+
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1405` |
101101
| 12 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `perf-timing.ts:474` |
102102
| 13 | Permission-set **overlay discard** skips the tenant-admin assertion | plugin-security | Get: an overlay can be discarded with no authenticated tenant administrator | `permission-set-overlay-discard.ts:142` |
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |

0 commit comments

Comments
 (0)