Skip to content

Commit 8ea4fd1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14010-hook-run-as-inherit
2 parents 78da2cb + 5bc2f27 commit 8ea4fd1

43 files changed

Lines changed: 2303 additions & 85 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os create example` now scaffolds a manifest the protocol schema accepts
6+
7+
The `objectstack.config.ts` that `os create example <name>` wrote declared
8+
three manifest keys — `name`, `version`, `description` — and nothing else.
9+
`ManifestSchema` requires `id` (the reverse-domain package id) and `type`
10+
(`app` | `plugin` | …), and `namespace` is the mandatory prefix of every object
11+
name, which decides each object's table name and REST path. Parsed against the
12+
schema, the emitted block answered `success: false` with
13+
`invalid_type@id · invalid_value@type`.
14+
15+
`defineStack` throws on exactly that, so the project a documented command had
16+
just created refused to load on its first run — before the author had written a
17+
line. The three `os init` templates all stamped the identity block; this was
18+
the one scaffold that had drifted, and nothing noticed because no test looked
19+
at these templates as data.
20+
21+
The template now stamps what `os init` stamps: `id`, `namespace` (derived from
22+
the project name with `init`'s own `sanitizeNamespace`, so both scaffolders
23+
answer the same way for the same input), `type: 'app'` and
24+
`engines.protocol`, alongside the `version`, `name` and `description` it
25+
already carried. `engines.protocol` is stamped from `PROTOCOL_MAJOR` — the same
26+
constant `init` stamps — and ships with the same self-contained comment
27+
explaining what the range is and when to move it.
28+
29+
A pin sweeps both scaffolders: every `init` and `create` template that emits an
30+
`objectstack.config.ts` is rendered through its own emitter, loaded back, and
31+
its `manifest` parsed through the real `ManifestSchema`. The population is
32+
derived from the two template maps rather than listed, so a template added
33+
later is swept the day it is added.
34+
35+
`os create` itself is untouched — it is not removed, deprecated, or redirected
36+
at `os init`. Whether the two scaffolders should stay separate is a CLI-surface
37+
decision, not this fix.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node
6+
7+
The published skill reference indexes and the generated `data/query` reference
8+
page opened on "Sort Node" — the description of a single `{ field, order }`
9+
pair — for the file that carries the entire `QueryAST`.
10+
11+
The generators publish the module's OWN doc block: top-level, in the header
12+
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
13+
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
14+
that block and its schema, so nothing attached it to a symbol. The mechanism was
15+
understood when the file was written — a warning comment sits directly under
16+
that block saying the first block becomes the page description. What was not
17+
noticed is the ORDERING: the first block belonged to a symbol, and a comment
18+
warning about a rule is not the same as satisfying it.
19+
20+
The file now opens with a short header of its own, reusing the sentence
21+
`QueryAST`'s own type block already carried. Four published skill indexes
22+
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
23+
and the reference page name the query AST as a result. `SortNode`'s block is
24+
untouched and still documents the schema it belongs to; the warning comment
25+
beside it now names which block is published and which selector picks it.
26+
27+
What the wrong row cost, in the skills' own terms: the skill tells an agent to
28+
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
29+
it skip the one file that carries the AST.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"@objectstack/plugin-sharing": minor
3+
"@objectstack/runtime": minor
4+
---
5+
6+
fix(sharing): gate the share-link route probe on `publicSharing.enabled`, at both probe sites (#14637)
7+
8+
**BREAKING** runtime behaviour change on a published HTTP path:
9+
`GET /api/v1/share-links/:token/resolve` answers `404 INVALID_OR_EXPIRED` where
10+
it used to answer `401 NEEDS_PASSWORD` / `401 WRONG_PASSWORD` /
11+
`401 SIGN_IN_REQUIRED` / `410 EXPIRED_OR_REVOKED`, for every link whose object
12+
has `publicSharing.enabled` switched off. Shipped as `minor` under the repo's
13+
launch-window convention (a breaking change does not burn a major while the
14+
stack is in lockstep). No published export is added, removed or re-shaped; the
15+
level carries the breaking banner, not a surface change.
16+
17+
#14033 made `publicSharing.enabled` a standing policy: `resolveToken()` re-reads
18+
the object's current block on every redemption and refuses a switched-off link
19+
with the same undifferentiated `null` a revoked, expired, unknown or ineligible
20+
token gets — because, in that gate's own words, for a caller who may hold
21+
nothing but a token a distinguishable "sharing is off for this object" is an
22+
**existence oracle**.
23+
24+
The HTTP layer above it then re-opened exactly that oracle. Both share-link
25+
surfaces run a row probe after `resolveToken()` returns null, to answer with a
26+
more useful status, and both answered from the `sys_share_link` row with no
27+
knowledge of the object's block. So an anonymous caller could still tell a
28+
real-but-switched-off token from an unknown one three ways: a row carrying
29+
`password_hash` drew `401 NEEDS_PASSWORD`, the same row with any password drew
30+
`401 WRONG_PASSWORD` — including a **correct** password, which is both an oracle
31+
and a lie, since that link can serve nothing — and a row with
32+
`audience: 'signed_in'` drew `401 SIGN_IN_REQUIRED`. A security property stated
33+
in one layer and defeated in the layer above it is worse than one never claimed,
34+
because the next reader believes the comment.
35+
36+
**What changed.** Both probes read the object's standing policy before they
37+
answer from the row, and when the block is off every arm falls through to the
38+
generic `404 INVALID_OR_EXPIRED` that unknown, revoked, expired and ineligible
39+
tokens already give — byte-for-byte the answer a token that never existed
40+
receives. The `410 EXPIRED_OR_REVOKED` arm is included: gating only the two 401
41+
arms would leave a third class of link answer and a rule about which arms are
42+
gated. An object whose schema the engine cannot answer for is `enabled: false`
43+
by `getPolicy`'s definition and is refused the same way — fail-closed, the same
44+
definition `createLink` and `resolveToken` already use.
45+
46+
The fix lands at **both** sites in one change, because the probe exists twice:
47+
`plugin-sharing`'s REST routes, and the `/share-links` dispatcher domain in
48+
`@objectstack/runtime` that is the designed primary surface for cloud's
49+
per-environment kernels (`registerShareLinkRoutes: false`). Fixing one would
50+
have moved the oracle to whichever embedding uses the other.
51+
52+
**Nothing else moves.** With the block ON, every refusal is exactly what it was:
53+
`NEEDS_PASSWORD`, `WRONG_PASSWORD`, `SIGN_IN_REQUIRED` and `EXPIRED_OR_REVOKED`
54+
are unchanged in status, code and message, and a correct password or a signed-in
55+
viewer still resolves the record. Mint-time behaviour is untouched, no
56+
`sys_share_link` row is written or read differently, and no error code is added
57+
or retired.
58+
59+
**Consumer impact.** A viewer that branches on the refusal STATUS sees TWO
60+
changes, for links on a switched-off object only — and the measured consumer
61+
branches on status alone. On the objectui console at `67dadd6`,
62+
`apps/console/src/pages/SharedRecordPage.tsx` lines 70-85 dispatch on
63+
`res.status` and never on the body's error code, so:
64+
65+
- all three 401 arms (`NEEDS_PASSWORD`, `WRONG_PASSWORD`, `SIGN_IN_REQUIRED`)
66+
rendered the password prompt and now render the 404 copy, "This link is
67+
invalid or no longer available.";
68+
- the 410 arm rendered "This link has expired or was revoked." and now renders
69+
that same 404 copy.
70+
71+
Both shifts are the intended outcome and were accepted with the ruling: a
72+
correct password on such a link yields nothing, so prompting for one teaches the
73+
holder to open a door that is bricked up, and "expired or revoked" is a claim
74+
about a token whose existence the caller must not be able to confirm. Links on
75+
objects whose block is on are unaffected — prompt, 410 copy and 200 render
76+
included.
77+
78+
Maintainer ruling 2026-09-03 (decision batch #17, item 1), verbatim 「同意」,
79+
adopting option A over option B (keep the 401 and document the accepted oracle)
80+
and option C (gate only the two 401 arms, rejected as proliferation).
81+
82+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-shaped: `publicSharing.enabled` keeps its name, type, default and accept-set, and this change is only WHICH HTTP STATUS the route layer answers with while that switch is off. There is no tombstone for `objectstack migrate meta` to carry and no mechanical rewrite it could perform on any consumer — a deployment that wants the 401 affordance back enables the object's block, which is an authoring decision, not a migration. -->
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
"@objectstack/platform-objects": minor
3+
"@objectstack/plugin-auth": minor
4+
"@objectstack/service-messaging": minor
5+
---
6+
7+
feat(platform-objects,plugin-auth): a user may set their own `sys_user.locale` (#14787)
8+
9+
Maintainer ruling 2026-09-03, option B, quoted verbatim and untranslated as
10+
adopted:
11+
12+
> 「同意」
13+
14+
The identity table's user-writable set grows from two fields to three. This is a
15+
security-boundary act, taken by the maintainer and recorded as one — it is the
16+
first widening of the ADR-0092 D2 self-service whitelist since that ADR shipped
17+
`{name, image}` as its first and only entry. `sys_user.locale` landed
18+
`readonly` and off the whitelist three weeks earlier (#13881 / #14775), which
19+
recorded a decision nobody had made yet; the ruling made it.
20+
21+
Three edits move together, and each one is inert without the other two:
22+
23+
- `SYS_USER_PROFILE_EDIT_FIELDS` becomes `{name, image, locale}`, so the
24+
identity write guard admits the column instead of stripping it (and, on a
25+
locale-only PATCH, throwing). `SYS_USER_IMPORT_UPDATE_FIELDS` inherits the
26+
widening by construction — it is a spread of the profile set, not a second
27+
list.
28+
- `MANAGED_EXTENSION_EDITABLE_FIELDS` gains a `sys_user` entry holding
29+
`locale` and nothing else.
30+
- `sys_user.locale` drops `readonly`. Without this the engine's readonly strip
31+
removes a caller-supplied value before the guard or the validator ever sees
32+
it, so the whitelist entry alone would have been a silent no-op.
33+
34+
**A malformed value is refused, not stored.** The column now declares a
35+
`locale_bcp47_shape` `format` validation rule carrying the same BCP-47 pattern
36+
the delivery-time reader uses, so objectql's rule validator rejects a malformed
37+
tag on insert, by-id update and bulk update with the standard
38+
`VALIDATION_FAILED` / `invalid_format` envelope (HTTP 400). The check is of
39+
SHAPE, not of membership: an unknown-but-well-formed tag is accepted and falls
40+
to the delivery ladder's floor rather than dead-lettering a notification, which
41+
is the property #13881's per-recipient chain was built to hold. An absent, null
42+
or empty column stays legal — clearing it is how a user returns to the
43+
deployment default, which remains the fallback.
44+
45+
**What did NOT widen.** The ADR-0092 D6 session-snapshot mirror keeps
46+
`{name, image}`: better-auth has no `locale` on its user model and it is
47+
deliberately not an `additionalFields` entry, so there is no cached copy to keep
48+
coherent, and merging one in would manufacture a `user.locale` key present only
49+
on sessions that happen to be cached and only after a profile edit. The mirror
50+
set is now named separately from the update whitelist rather than derived from
51+
it.
52+
53+
**Who may perform the write is unchanged, and is a separate question.** ADR-0092
54+
D5 leaves that with the permission layer: `member_default` still denies
55+
`allowEdit` on `sys_user`, so a rank-and-file member reaches this column through
56+
no shipped surface yet — the widening opens the COLUMN, not a self-service
57+
route. Granting one (the `sys_api_key` shape: an explicit `member_default` entry
58+
plus a `_self` row-scope for writes) is a further security-boundary decision
59+
that this ruling did not take.
60+
61+
The `identity-write-guard` and `managed-extension-fields` pins that recorded the
62+
old posture are FLIPPED, not deleted, each naming the ruling that reversed it —
63+
a pin that recorded a real decision is evidence, and evidence of a superseded
64+
decision is what tells the next reader the reversal was deliberate.
65+
66+
`@objectstack/service-messaging` is a docs-and-export change only: its
67+
`LOCALE_TAG_SHAPE` is unchanged in behaviour and now exported so a parity pin
68+
can hold it byte-identical to the write-side pattern. Read-side normalization
69+
stays — it is strictly the stricter of the two (`"null"` is shape-legal and only
70+
the read side refuses it) and it guards values that arrive below the data API,
71+
where no write rule runs.

.claude/agents/os-dev.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ dispatch prompt 只携带每单增量(裁决引文、裁决 / PM-机制假设分
228228
`origin/main` 是共享指针,别的 agent 一次 fetch 就推进它:`git reset --soft origin/main` 把你分支点
229229
之后**他人已合并的文件**整批 stage 成你的(实测一次四个 agent 的合并文件,commit 前才逮
230230
住)。「我从哪开始」的 reset/diff/log/rebase 一律锚基本规则 1 记录的 `"$BASE"`
231+
- **`log -S/--follow/blame` 判日期或先后前**,先 `git rev-parse --is-shallow-repository`,true 就加深申报。
231232
- **要做反向验证(「回退修复,看诊断变化」)?先 commit 修复。** 已 commit,恢复只是
232233
`git checkout <your-branch> -- <path>`;对着未提交的编辑, `git checkout origin/main -- <path>` 不留任何恢
233234
复点 —— 工作树曾是唯一副本,而丢弃它是一次正常、无声、exit-0 的操作(恢复机制与字
@@ -299,7 +300,6 @@ dispatch prompt 只携带每单增量(裁决引文、裁决 / PM-机制假设分
299300
- 实现满足 issue 的验收判据。
300301
- 测试:新增/更新覆盖;跑受影响包的 `pnpm test` / `pnpm typecheck`,为报告留真实输出 (范围
301302
按「本地验证范围」圈定)。
302-
- 用户可见的改动加 changeset。
303303
-`git push -u origin claude/issue-<n>-<slug>` 推上去(网络失败退避重试)。
304304
- **Draft** PR 指向 `main`,正文首行 `Fixes #<n>` —— **合并不应关卡时用 `Part of #<n>`**(你只实现
305305
了可执行的那一半;说明留下的是哪一半)。⛔ 永不 `Fixes` 一张还在决策箱的卡 —— 合并

.claude/skills/pm-dispatch/references/lanes/cli.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414

1515
## 常设承诺
1616

17-
- **Required checks 六个**:`TypeScript Type Check` · `Lint & Repo Gates` ·
18-
`Test Core` · `Dogfood Regression Gate` · `Build Core` ·
19-
`Temporal Conformance (live PG + MySQL)` —— 逐 job 读各自 `conclusion`,⛔ 不认
20-
聚合,`in_progress` 不是过;advisory 门禁红进 main 是共享损伤,照样止血立单。
2117
- **PR 侧绿 ≠ 队列侧绿**;队列分支读法与边界在 platform-readings(条目名
2218
`main/pr-{number}-{parent sha}`,parent sha 可重建队序;⚠️ ref 在出队后滞留)。
2319
- `dispatch-gates.mjs` 只报**路径推导**的地板 —— 条款②从卡**内容**判且优先于它;已知盲

.claude/skills/pm-dispatch/references/lanes/services.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
—— 单独的绿不是证据。
1919
- **安全族卡的披露纪律**:复现配方不落任何公开面(卡、PR、评论);证据以抽象描述
2020
或私有通道承载。
21-
- 门禁读数不轻信聚合:`check:type-check-debt` 可以在包级 typecheck 绿时红;
22-
`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 3 不是漂移。
2321

2422
## 席内判断
2523

.claude/skills/pm-dispatch/references/platform-readings.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,16 @@
294294
降档(那正是降档保险丝要拒的替换),但「本车道 fable 强制」多是过宽的回忆 ——
295295
`dispatch-gates.mjs --tier PATH` 逐路径现推(实测:`SKILL.md``.claude/agents/**` 强制,`scripts/pm/**`
296296
`references/**` 无 ⇒ 全阻塞的三卡 fold 拆成 1 阻 3 可跑);路径线是**下限不是放行**
297+
- **required checks 的名单是每仓事实**(逐 job 读判定的规则住上面队列段):objectstack 六个 ——
298+
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Dogfood Regression Gate` ·
299+
`Build Core` · `Temporal Conformance (live PG + MySQL)`;`in_progress` 不是过;advisory 门禁
300+
红进 main 是共享损伤,照样止血立单。⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在
301+
包级 typecheck 绿时红;`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 3 不是漂移。
302+
- ⚠️ **计数不是机理读数(被当天推翻的推断的墓碑)**:「`GET .../actions/runs?event=merge_group`
303+
计数 0 ⇒ required 集为空」提出当天即被自身推翻 —— 同一姊妹仓 2026-08-24 首现 merge_group
304+
run(0 → 8),同日再测 224(阳性对照 `event=pull_request` 全程非零)。计数答「至今发生过没有」,
305+
不答「机制在不在」:零计数只作**弱先验**,判 required 集为空要读 ruleset
306+
的 required 集本身、或看队列合并是否真在等检查;⛔ 别处写下的计数值一律先复测再用。
297307

298308
## 闭合关键词解析(PR 正文写侧)
299309

.claude/skills/pm-dispatch/references/rest-channel.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,6 @@ payload → REST → MCP/GraphQL** 的策略住那里,本表是逐操作的通
7676
`PUT .../pulls/{n}/merge`,强制队列 ruleset 下回 **405 `Changes must be made through the merge queue`**;② PR
7777
上的 `added_to_merge_queue` timeline 事件;③ 对已入队 PR 调 update-branch 回「已入队分支不能更新,
7878
要改先出队」。①② 拼写与边界是 `platform-readings.md` 队列段既有行,本条只归拢判据。
79-
- ⚠️ **计数不是机理读数(被当天推翻的推断的墓碑)**:「`GET .../actions/runs?event=merge_group`
80-
计数 0 ⇒ required 集为空」提出当天即被自身推翻 —— 同一姊妹仓 2026-08-24 首现 merge_group
81-
run(0 → 8),同日再测 224(阳性对照 `event=pull_request` 全程非零)。计数答「至今发生过没有」,
82-
不答「机制在不在」:零计数只作**弱先验**,判 required 集为空要读 ruleset
83-
的 required 集本身、或看队列合并是否真在等检查;⛔ 别处写下的计数值一律先复测再用。
8479
- **required job 名与分片矩阵的改名耦合(现行,自 2026-08-24)**:
8580
队列 required 集按 **job / check-run 名**匹配,**workflow 名从不作为 check context 出现**(所以拿
8681
workflow 名在选择器里搜什么也搜不到);改其中任一 job 名**或 test 分片矩阵的形状**,

content/docs/permissions/tenant-audit-census.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ receiver that none of the three place is an error, never a default.**
8484

8585
Tenancy itself is enabled *by default*`isTenancyDisabled()` reads
8686
`tenancy.enabled === false` and nothing else — so the object registry only has to
87-
find the opt-outs. Across 297 declared objects — the dated, ⛔ unenforced
87+
find the opt-outs. Across 298 declared objects — the dated, ⛔ unenforced
8888
corpus-scale figure below — exactly two opt out (`sys_api_key`,
8989
`sys_sso_provider`), and no write call site on this surface targets either.
9090

@@ -224,13 +224,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
224224
their values are not compared. The reasoning, and the measurement behind it,
225225
are in `scripts/check-tenant-audit-census.mjs`.
226226

227-
Measured on 2026-09-03 at `98b1cf0b7`.
227+
Measured on 2026-09-03 at `631038b03`.
228228

229229
| corpus scale (not enforced) | count |
230230
| :--- | ---: |
231-
| tracked non-test sources scanned | 540 |
231+
| tracked non-test sources scanned | 542 |
232232
| engine-shaped types recognised | 57 |
233-
| declared objects in the registry | 297 |
233+
| declared objects in the registry | 298 |
234234
| same-named calls subtracted as non-engine | 130 |
235235

236236
{/* END GENERATED: tenant-audit-census */}

0 commit comments

Comments
 (0)