Skip to content

Commit 90097ce

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-6290-current-user-scope-roots
2 parents ea23522 + a36db28 commit 90097ce

130 files changed

Lines changed: 8331 additions & 1153 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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/driver-sql": patch
4+
---
5+
6+
fix(objectql,driver-sql): 自增号播种按声明的 `suffix` 定位计数器,两侧收敛到同一答案 (#6468)
7+
8+
`autonumberFormat` 允许序号槽 `{0..0}` **后面**还有 token —— `renderAutonumber`
9+
专门返回 `suffix`,其契约就是 `prefix + zero-padded(seq) + suffix`。这类格式渲染
10+
出的值**序号不在串尾**:`{000}-{YYYY}` 渲染成 `001-2026`,是很常见的单号写法。
11+
12+
两侧的播种解析却都假定「串尾的数字就是计数器」,而且各错各的:
13+
14+
- 引擎兜底播种 `seedAutonumber()` 取整串的**最后一个**数字段 —— 读到的是年份。
15+
库里三行 `001-2026`/`002-2026`/`003-2026`(真实计数器 3)把计数器播种成 **2026**,
16+
下一个发出的号直接跳到 `2027-2026`;
17+
- driver-sql 的 `scanMaxNumericTail()` 把 tail 里**所有**数字拼接后 `parseInt` ——
18+
同样三行读成 **12026**,下一个号是 `12027-2026`
19+
20+
于是**同一份元数据、同一批行,换个驱动号段就不一样**;中间跳过的号已经烧掉,事后
21+
无法回收。只修一侧会把「两个不同的错误答案」变成「一个对一个错」,跨驱动仍不一致,
22+
所以两侧同 PR 修。
23+
24+
**修法:两侧解析器尊重已声明的 `prefix`/`suffix`** 两个字符串都由调用方从
25+
`renderAutonumber` 的返回值取得后传入 —— 两侧都不再自行理解格式,driver-sql 只收
26+
参数(`getNextSequenceValue` 仅多转发一个位置参数,序列逻辑本身未动):
27+
28+
- **prefix / suffix 任一非空 ⇒ 计数器「有锚」**:取 prefix 之后的**首个**数字段,
29+
并在该行确实带有声明的 suffix 时先把它去掉;
30+
- **两者皆空 ⇒ 「无锚」**:各自的既有读法**逐字保留**(引擎取整串最后一个数字段,
31+
driver-sql 拼接全部数字)—— 无 `{0..0}` 槽的格式渲染的就是串尾裸计数器,而早于
32+
格式存在的历史值根本没有锚可依。
33+
34+
**suffix 只在匹配时剥离,绝不要求匹配。** `{000}-{YYYY}` 的计数器 scope 是渲染后的
35+
**prefix**(此处为空),即全局一个计数器、只有显示的年份在变,所以去年的 `007-2025`
36+
持有计数器 7,必须计入。把 suffix 下推成 `like '%-2026'` 会把这些行整批漏掉、播种
37+
**低于**真实 max —— 那正是 #6249 修掉的重复单号伤害,自己再造一遍。因此 SQL 谓词
38+
保持 `like 'prefix%'`,suffix 只在 JS 侧逐行使用。
39+
40+
无后缀格式(`D-{0000}``{0000}`)两侧本来就正确,行为不变并已 pin 住;#6467
41+
播种扫描结构未触碰。
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
fix(spec,metadata-protocol): `capability` 补齐三处注册 —— 授权面不再接受任意 JSON (#5961)
7+
8+
`capability` 是「enforced but undeclared」——#5271`api` 关掉的那个
9+
`declared ≠ enforced` 的镜像。平台早就把它当成一个 metadata kind 在用:
10+
`PLURAL_TO_SINGULAR`#5870 起就有 `capabilities``capability`,
11+
`AppPlugin` 用这个名字注册 stack 声明的 capability,
12+
`bootstrapDeclaredCapabilities` 再读回来 seed `sys_capability`。但三处注册表
13+
里都没有它:`MetadataTypeSchema`(kind 枚举)、`BUILTIN_METADATA_TYPE_SCHEMAS`
14+
(schema 解析)、`DEFAULT_METADATA_TYPE_REGISTRY`(谁可以写、怎么加载)。
15+
16+
后果有两条,第二条才是这个 issue 属于授权缺陷而非整洁度问题的原因:
17+
18+
- `getMetadataTypeSchema('capability')` 返回 `undefined`,于是 `saveMetaItem`
19+
走了它自己文档化的「未注册类型 → 不校验直接存」分支,
20+
`PUT /api/v1/meta/capability/:name` 接受**任意 JSON** 落进 `sys_metadata`
21+
capability 是靠**名字字符串**被解析的——授予侧 `systemPermissions`
22+
要求侧 `requiredPermissions` 都是——所以一行任意 JSON 直接落在活的授权命名
23+
空间里。
24+
- `isRuntimeCreateAllowed` 镜像 `getMetaTypes()` 的合成规则:没有静态注册表条目
25+
的类型被当作可运行时创建。所以缺的那一行不只是「没关上门」,它**把门打开了**
26+
`/meta/types` 同步发布了这个虚构:`allowRuntimeCreate: true` + 无 schema,
27+
metadata-admin 引擎据此渲染成一个 raw-JSON 文本框。
28+
29+
### 改了什么
30+
31+
- **`BUILTIN_METADATA_TYPE_SCHEMAS['capability'] = CapabilityDeclarationSchema`**
32+
既有的 422 `invalid_metadata` 路径就此覆盖 `capability`,`/meta/types` 发出真
33+
JSON Schema。
34+
- **`DEFAULT_METADATA_TYPE_REGISTRY` 新增 `capability` 条目,
35+
`allowRuntimeCreate: false` + `allowOrgOverride: false`**。ADR-0066 D1:包
36+
DEFINE capability,权限集 GRANT,资源 REQUIRE。管理员在运行时凭空造一个
37+
capability 在这个三分里没有位置——代码里不会有任何地方 require 那个名字,这行
38+
只是授权命名空间里一个无人引用的授予目标。这一对标志就是 #5086 的 CODE-ONLY
39+
声明,`saveMetaItem`**任何** kernel 上都以 403 `not_creatable` 拒绝,并从条
40+
目自己的 `filePatterns[0]` 读回「该去哪儿声明」。`supportsOverlay: false`——
41+
capability 只是名字/标签/scope,没有 merge 语义,而允许租户 overlay 一个包发布
42+
的声明等于允许把 `scope``org` 抬成 `platform``loadOrder: 12` 早于
43+
`permission`/`position`(15),使权限集的 `systemPermissions` 解析时 capability
44+
已经存在。
45+
- **`MetadataTypeSchema` 枚举补 `'capability'`**
46+
- **`CapabilityDeclarationSchema` 声明 ADR-0010 保护信封并收紧为 `.strict()`**
47+
信封是必须的:loader 对每个已注册类型都调 `applyProtection`,不声明就会 422 掉
48+
loader 自己的输出(#4001`permission`/`position` 上补过同一个洞)。收紧则与
49+
`api` 不同——`ApiEndpointSchema` 同时是**存储行**的解析器,所以它留在
50+
`STILL_STRIP`;而没有任何地方拿这个 schema 重新解析 `sys_capability`
51+
(`bootstrapDeclaredCapabilities` 通过 `capabilityRowFields` 按名读字段),
52+
所以收紧零成本,买到的是一个授权面本就该有的 declared = enforced 姿态。
53+
改用 `strictObject` 书写,已知键从 shape 派生,不新增手抄键表。
54+
55+
**包声明通道完全没动。** `AppPlugin` 通过 `registerInMemory` 注册 stack 的
56+
`capabilities[]`,文件系统 loader 按 `filePatterns` glob——两条都不经过
57+
`saveMetaItem`,所以 `bootstrapDeclaredCapabilities` 依旧照常 seed。
58+
`OS_METADATA_WRITABLE=capability` 仍是 ADR-0005 那唯一一道运维逃生门,而在它后面
59+
写入现在由 `CapabilityDeclarationSchema` 判定(422),不再原样落盘。
60+
61+
`role` / `profile` / `policy` **不搭车**:它们没有 `PLURAL_TO_SINGULAR` 映射、
62+
没有声明 schema、没有读回接缝,是另一个问题,另开单。这条以断言形式钉在
63+
`capability-metadata-kind.test.ts` 里,因为「capability 有了条目,邻居也该有」
64+
正是下一个显而易见却错误的改动。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/metadata-protocol": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
fix(spec,metadata-protocol,runtime): one place decides what an unset `NODE_ENV` advertises (#5936)
8+
9+
A deployment whose operator never exported `NODE_ENV` must not describe itself as
10+
`development` on `/discovery`: `environment` is a machine-readable field, a client
11+
reads it to answer "am I talking to production?", and it may skip production warnings
12+
or loosen a destructive action's confirmation on the answer. #5673 ruled that in and
13+
fixed it — but only for one of the two producers, because that dispatch put
14+
`packages/spec` out of scope. The other one, `MetadataProtocol.getDiscovery()` (served
15+
by `@objectstack/rest`), went on answering `development` for exactly that input.
16+
17+
The default now lives in the shared mapper, `resolveDiscoveryEnvironment`: an absent —
18+
or blank — value resolves to `production`, and both producers pass the operator's value
19+
through as they read it, neither carrying a default of its own. That is what makes it
20+
one decision instead of two copies, and it means the next discovery producer inherits
21+
the right answer without anyone remembering to copy a line. Patching only
22+
metadata-protocol would have left a second copy of the default — precisely the drift the
23+
shared table was created to prevent (#4828).
24+
25+
"Unset" includes a blank value: `NODE_ENV=` exports an empty string, the runtime's
26+
`getEnv` has always folded that into its default, and had the mapper treated blank as
27+
"anything else" the two producers would have drifted again on that one input.
28+
29+
**#4828's rule is untouched, and it points the other way on purpose.** A value that IS
30+
set but is not a spelling this repo recognises (`qa`, `preview`) still degrades to
31+
`development`, so nothing ever claims `production` on a guess. Absence is not a guess —
32+
it is the host declining to say.
33+
34+
Behaviour change to expect: a host that exports no `NODE_ENV` and serves `/discovery`
35+
through `@objectstack/rest` now advertises `environment: "production"` where it
36+
previously advertised `"development"`. A deployment that genuinely is development should
37+
say so — `NODE_ENV=development` — which is what the runtime dispatcher has already
38+
required since #5673.
39+
40+
The mapping table above `NODE_ENV_TO_DISCOVERY_ENVIRONMENT` is corrected in the same
41+
pass: its `unset / anything else -> development` row had been false for the runtime
42+
caller since #5673 and is now two rows, one per rule.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot
7+
8+
**`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain
9+
`z.string()` while three published platform pages authored 31 inline
10+
`{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through
11+
`pickLocalized` — so the authoritative document was the wrong one, and the
12+
#5068 component-props gate reported 42 findings against the platform's own
13+
pages. The schema is now a union of the default-language string and an inline
14+
locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()`
15+
and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the
16+
same change, which is the other 8 of the 42. The gate now reports **0**.
17+
18+
This does not reverse #4667 / #5055. What those retired was the *key-reference*
19+
dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label
20+
reached the screen as a raw key or not at all. What is authorized here is the
21+
inline locale map, which has a live resolver and which the CLI's `i18n-extract`
22+
already understands. Same "declared = enforced" principle, applied in both
23+
directions: the map's keys are constrained to BCP-47 tags (plus `default`), so
24+
`{ key, defaultValue }` stays a parse error rather than becoming "a locale map
25+
whose locales are named `key` and `defaultValue`".
26+
27+
Zero breaking: every previously-valid label is still valid. The
28+
translation-bundle channel remains the direction that scales and is unchanged.
29+
30+
**Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData`
31+
gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads
32+
it — explicit `_tabs` translation, then the referenced view's `_views.*.label`
33+
for a tab that carries `view` (the path that already worked, preserved), then
34+
the authored literal. A tab carrying only a `filter` referenced nothing to
35+
inherit from and had no key of its own, so its label rendered in the source
36+
language above a fully localized grid with no authoring workaround. `os i18n
37+
extract` scaffolds the new keys, so the slot, the resolver and the extractor
38+
land together.
39+
40+
`I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated
41+
by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no
42+
translation-bundle slot addresses it.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/objectql": patch
4+
---
5+
6+
fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633 ruling D)
7+
8+
`POST /api/v1/data/:object/import?dryRun=true` green-lit rows the very same
9+
endpoint then rejected. Measured on 17.0.0-rc.1: a CSV cell aimed at a
10+
structured `address` field reported `{ ok: 1, created: 1 }` on the dry run and
11+
`{ errors: 1, code: 'VALIDATION_FAILED' }` on the real write.
12+
13+
The dry run predicted the write's verdict with a hand-copied mirror of a slice
14+
of the engine's rules (`import-coerce.ts`'s `firstMissingRequiredField` and
15+
`firstConstraintViolation`). A copy cannot structurally keep up with the family
16+
it mirrors: ADR-0104 value shapes (`address` / `location` / references / media),
17+
`format` checks, object-level `validations` and the state machine had no
18+
counterpart, and `coerceFieldValue` routes structured shapes through its
19+
pass-through catch-all, so no verdict was formed at all.
20+
21+
**The mirror is retired.** The dry run now calls `DataProtocol.validateData`
22+
(#6037), which runs the same `validateRecord` / `evaluateValidationRules` that
23+
`insert()` runs, under the deployment's own ADR-0104 posture — so a bad value
24+
shape is an error on a self-certified deployment and an admitted warning on a
25+
warn-first one, exactly as on the write. Agreement is by construction, not by a
26+
copy kept in step by hand.
27+
28+
Also in this change:
29+
30+
- **`engine.validate()` now resolves `defaultValue`s and seeds owned roll-up
31+
`summary` fields before validating, on `insert` mode**, because `insert()`
32+
does. Without it a required-but-defaulted column left unmapped was previewed
33+
`failed` and written `created` — a false alarm on the row a preview is meant
34+
to reassure you about. `update` mode still does not default (#2706).
35+
- **A row report failed by validation now names the offending column.** The
36+
engine's `ValidationError` carries `fields[]`, so the row's `field` is set and
37+
its `code` is the field-level code (`required`, `min_value`, `max_length`,
38+
`invalid_type`, …) rather than the wrapper's `VALIDATION_FAILED`. This is the
39+
same vocabulary the dry run and the per-cell coercion failures already spoke;
40+
before, a `min: 0` violation was `min_value` on the dry run and
41+
`VALIDATION_FAILED` on the write.
42+
- **Dry-run rows may carry `warnings[]`** — findings this deployment admits
43+
rather than rejects (ADR-0104 warn-first). The row is `ok`, and the complaint
44+
is visible instead of living only in a server log line.
45+
46+
A protocol that does not implement `validateData` (plugin-auth's identity
47+
import, whose write is better-auth rather than the engine) is not handed a
48+
substitute: its dry run reports coercion and create/update/skip resolution only.
49+
An engine-derived preview of a non-engine write would report findings that write
50+
never produces.

.changeset/lucky-pears-arrive.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `--json` now owns stdout — kernel boot logs move to stderr (#6217)
6+
7+
Every `os migrate` / `os meta` subcommand that boots a kernel wrote its
8+
machine-readable payload into a stream it shared with ~60 INFO lines. The
9+
kernel logger routes `debug`/`info`/`warn` to stdout and only `error`/`fatal`
10+
to stderr, so `os migrate recorded-by --json | jq .` failed with `parse error:
11+
Invalid numeric literal` while stderr sat completely empty — a `--json` flag
12+
whose only audience is a program, handing that program something it cannot
13+
parse.
14+
15+
With this change, a `--json` run reserves stdout for its payload: everything
16+
the kernel and its plugins write goes to **stderr** instead, including the
17+
`[StandaloneStack] no compiled artifact …` notice that never went through the
18+
logger at all. `JSON.parse(<entire stdout>)` now succeeds with no heuristic
19+
extraction, and no diagnostic is lost — every line an operator used to see is
20+
still printed, on the stream diagnostics belong on.
21+
22+
Covers the whole family that shares the boot seam: `os migrate plan` / `apply`
23+
/ `resume` / `recorded-by` / `summary-nulls` / `value-shapes` /
24+
`files-to-references`, `os migrate meta --stored`, and `os meta resync`.
25+
Human-mode runs are unchanged.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/mcp': patch
3+
---
4+
5+
mcp: a metadata outage stops being reported to MCP clients as `Agent "X" not found`
6+
7+
The `agent_prompt` prompt resolved its body through `metadataService.get('agent', name)`
8+
and answered the resulting `undefined` with `Error: Agent "X" not found`. That `undefined`
9+
carries two opposite facts (#5840, ADR-0110 D3): the name was never declared, or every
10+
loader behind the metadata service was down. So during a metadata outage an MCP client was
11+
told, positively, what the author had declared — from a read that never happened. The same
12+
shape sat one bridge over: the `objectstack://objects/{objectName}` resource answered
13+
`getObject()`'s `undefined` with `Object "X" not found`.
14+
15+
**Both surfaces now separate the two.** A degraded read answers `SERVICE_UNAVAILABLE`
16+
the same catalogued code and the same "whether it exists is unknown, retry once it is
17+
reachable" sentence the `sys_metadata` half of this family already emits (#5532 / #5843) —
18+
and a genuine miss keeps its not-found answer, byte for byte on the prompt surface.
19+
MCP's `prompts/get` and `resources/read` results carry no error envelope, so the
20+
classification travels in the payload each surface already had: the prompt's text, and the
21+
resource's JSON body, which now names `code` and `status` on **both** answers
22+
(`SERVICE_UNAVAILABLE`/503 vs `RESOURCE_NOT_FOUND`/404) so a client can tell them apart
23+
without parsing prose.
24+
25+
**This is a diagnosis fix, not an access change.** Both surfaces were already fail-closed:
26+
no instructions and no schema were served during an outage before this, and none are now.
27+
The defect was the description.
28+
29+
Hosts whose `metadata` slot predates the optional `getDiagnosed` member report nothing
30+
degraded — exactly what they could express before — so their behaviour is unchanged. The
31+
object resource additionally keeps `getObject()` as its resolver and consults the
32+
diagnosed read only as a verdict probe on the miss path, because `getObject` is its own
33+
contract member with no documented equivalence to `get('object', name)` (and
34+
`MetadataFacade.getObject` is not that).

0 commit comments

Comments
 (0)