Skip to content

Commit 00d5635

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-h5z02d
2 parents 80e07fa + e1e9022 commit 00d5635

21 files changed

Lines changed: 1035 additions & 849 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
docs: the external org-wide-default floor's docblock no longer lists `controlled_by_parent` among the models it closes
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`owdForAudience`'s module docblock said an external is floored on a `controlled_by_parent` detail. It is not: plugin-sharing exempts the detail, whose rows follow its master, and the master meets the floor (#25541). Comment only; no behaviour changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the objectstack-data skill's `current_user` placeholder table names a declared membership key
6+
7+
Clause-②: no
8+
9+
The security rules' table of the `current_user.*` placeholders the RLS compiler resolves listed only the kernel's keys. It now has a row for `current_user.KEY`, the set an object's `membershipKeys.KEY` declares (for `IN`), so an author reading the catalog alone learns that a declared key resolves.

‎.claude/skills/pm-dispatch/references/contract-review.md‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,8 @@
3333
只读条款与输入集内建,⛔ 不删 ⛔ 行。
3434
- 隔离复核子代理暂存全写按所审 PR 命名的 `<scratchpad>/pr-<n>/`,⛔ 不读非本轮自写的暂存。
3535
- 独立性对:`Implemented-by:`/`Reviewed-by:` 取值同见 `--template`;子代理记采纳它的席位。
36-
- 项目席落地的 Tier S:`Reviewed-by:` 须属 PR 作者外的账户;同账户子代理只算复核。
37-
- 作者账户 = PR 正文 attribution 的 `github_login`;记录另带 `Reviewed-by-account:` 行写复核账户。
38-
- 只动散文面、不欠契约复核的 PR 同欠另一账户一条 `TIER_DEFAULT` 记录;⛔ 散文面无豁免。
3936
- 轮次报告设复审清单专节,与呈裁清单同为强制审计。
4037
- 落地前检两条,过则 Tier S 入队、Tier H 等人批:① 达档条款②复核 PASS 在案(同形记录)。
41-
- ① 在项目席另加:`Reviewed-by-account:` 的登录须异于 PR attribution 的登录,队列守卫同判。
42-
- 过渡:无另一账户在席时,① 的替代 = 维护者本人在执行线程的话:逐个点名 PR 编号,
43-
或对该席位的 Tier S 长期授权(一句可撤)。
4438
- 例外:纯重生成或只动上列散文面的 head 后移原记录继续管;判据机读已提交树;
4539
PR 落 `Regen-provenance:` 行。
4640
- ② PR check 全绿,⛔ 非 required 子集;例外:merge-base 同签名的红不计、按设计而红见 SKILL.md。

‎content/docs/protocol/backward-compatibility.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ObjectStack follows strict backward compatibility guarantees to ensure predictab
1919

2020
**The v18 line has exited prerelease mode.** While Changesets was in pre mode with the tag `next`, every release cut from `main` was the next `18.0.0-next.N` on npm's `next` tag, and `scripts/check-changeset-no-major.mjs` stood aside in pre mode, so a pull request could declare a major bump. Changesets has exited pre mode, so the line's GA version is `18.0.0`, which publishes to npm's `latest` tag, and the guard again fails a pull request that declares a `major` bump. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line).
2121

22-
**After 18.0.0, a breaking change ships in a MINOR or a PATCH release.** The maintainer's 2026-10-21 ruling ([#25105](https://github.com/objectstack-ai/objectstack/issues/25105)) keeps the no-major guard and grades each change by the metadata protocol: a key, value or metadata type added, narrowed or given a new meaning takes MINOR, and every other change takes PATCH, breaking or not. A non-protocol breaking change (a REST response, a boot refusal, a CLI exit status, a removed runtime export) keeps its `**BREAKING**` banner and ships as a PATCH. 18.1.0, the first release under the rule, is a MINOR that carries 87 changesets declared breaking.
22+
**After 18.0.0, a breaking change ships in a MINOR or a PATCH release.** The maintainer's 2026-10-21 ruling ([#25105](https://github.com/objectstack-ai/objectstack/issues/25105)) keeps the no-major guard and grades each change by the metadata protocol: a key, value or metadata type added, narrowed or given a new meaning takes MINOR, and every other change takes PATCH, breaking or not. A non-protocol breaking change (a REST response, a boot refusal, a CLI exit status, a removed runtime export) keeps its `**BREAKING**` banner and ships as a PATCH. 18.1.0, the first release under the rule, is a MINOR that carries 90 changesets declared breaking.
2323
</Callout>
2424

2525
ObjectStack follows [Semantic Versioning 2.0.0](https://semver.org/) (`MAJOR.MINOR.PATCH`). The table below describes what each component means; which version a breaking change ships in depends on the line, as the callout above says.

‎content/docs/releases/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ meaning takes `minor`, and every other change takes `patch` — a REST door, a
7272
driver, a boot refusal, the CLI or the Console — breaking or not. A
7373
non-protocol breaking change keeps its `**BREAKING**` banner and its ADR-0087
7474
disposition. 18.1.0 is the first release cut under the rule: a minor carrying
75-
87 changesets declared breaking, 39 of them graded `patch`.
75+
90 changesets declared breaking, 42 of them graded `patch`.
7676

7777
What that means when you read a version number:
7878

‎content/docs/releases/v18/18-1.mdx‎

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,22 @@ description: "Release notes and upgrade checklist for 18.1.0, the first minor of
55

66
<Callout type="warn">
77
**How this page was compiled.** From the 18.1.0 changesets on `main` at
8-
`03243fc637` (2026-10-24), before the 18.1.0 cut: a rehearsed
8+
`ba9e0c3749` (2026-10-24), before the 18.1.0 cut: a rehearsed
99
`pnpm run version` in a throwaway clone at `b2c0752900` (73 packages move to
10-
18.1.0, and their `## 18.1.0` sections held 227 distinct entries), and the 26
11-
changesets merged after it, graded the same way. That makes **253 distinct
10+
18.1.0, and their `## 18.1.0` sections held 227 distinct entries), and the 31
11+
changesets merged after it, graded the same way. That makes **258 distinct
1212
entries**. The five sections below use the grouping the GitHub Release body
13-
uses, and the counts are that grouping's: **78** need you to act, **9** are
14-
converted for you, **47** add capabilities, **2** declare a semantic change,
15-
and **117** are other fixes. Nothing here was
13+
uses, and the counts are that grouping's: **81** need you to act, **9** are
14+
converted for you, **47** add capabilities, **3** declare a semantic change,
15+
and **118** are other fixes. Nothing here was
1616
read off a published 18.1.0 package or walked on an upgrade yet: every refusal
1717
is described as its changeset describes it. Anything that lands on `main` before
1818
the cut is in the CHANGELOG and not on this page.
1919
</Callout>
2020

2121
## Highlights — 18.1.0
2222

23-
- ⚠️ **A minor that breaks.** 18.1.0 is a minor version, and 87 of its
23+
- ⚠️ **A minor that breaks.** 18.1.0 is a minor version, and 90 of its
2424
changesets are declared breaking. Under the level rule the maintainer ruled
2525
on 2026-10-21 (#25105, "ruling A"), the v18 line takes no further major: a
2626
change to the metadata protocol takes `minor`, and every other change takes
@@ -88,7 +88,7 @@ else:
8888
| Everything else: REST, drivers, services, the CLI, the Console, `feat:` and `fix!:` alike | `patch` |
8989

9090
So a **patch** release can break a REST client, a boot or a deployment
91-
script, and 18.1.0 carries 39 changesets graded `patch` that say **BREAKING**,
91+
script, and 18.1.0 carries 42 changesets graded `patch` that say **BREAKING**,
9292
beside 48 graded `minor` ones. Pin exact `@objectstack/*` versions, and read the
9393
release notes for every version you cross. Each entry in a CHANGELOG states its
9494
kind on its own `Metadata-protocol:` line, and the GitHub Release body groups the
@@ -102,7 +102,7 @@ you change them.
102102

103103
### Breaking changes you must act on
104104

105-
The 78 entries that are breaking and have no ADR-0087 conversion, and one boot
105+
The 81 entries that are breaking and have no ADR-0087 conversion, and one boot
106106
refusal (`db1c7cc`) that its changeset declares as an addition. Each line names
107107
the one fix; the CHANGELOG entry carries the full FROM → TO.
108108

@@ -116,6 +116,14 @@ the one fix; the CHANGELOG entry carries the full FROM → TO.
116116
is too. Fix: set `OS_AUTH_SECRET`, and enable auth on a stack that mounts
117117
none. A development boot (`os dev`, `--dev`) and an embedded kernel are not
118118
affected.
119+
- **A boot with no `OS_AUTH_SECRET` uses the temporary secret on a development
120+
boot only** (`6abee03`, #25563). It used it whenever `NODE_ENV` was anything
121+
but exactly `production` (unset, `staging`, a typo); now any boot other than
122+
`NODE_ENV=development` or `test` throws `OS_AUTH_SECRET is required outside
123+
development`. `os serve` and `os start` already default an unset `NODE_ENV`
124+
to `production`, so CLI boots behave as before: an embedder that builds its
125+
own kernel is the one that meets it. Fix: set `OS_AUTH_SECRET`, or
126+
`NODE_ENV=development` for a local boot.
119127
- **A production `os serve` whose `@objectstack/plugin-auth` fails to load is
120128
refused, naming the load error** (`b2e964f`, #25231), `os start` included.
121129
18.0.0 booted it without auth, silently or with one
@@ -253,6 +261,14 @@ door (`422 INVALID_METADATA`), where 18.0.0 accepted it.
253261
starts empty, and a read-only boot no longer opens an empty in-memory database
254262
over a file it cannot open or create (`4d99158`, #25007). Fix: restore from a
255263
backup, or move the file aside yourself; an absent file is created empty.
264+
- **`SqlDriver.findWithWindowFunctions()` serves a closed set** (`6abee03`,
265+
#25563), and so do `driver-sqlite-wasm` and `driver-turso`'s local and
266+
replica transports: the function is one of `row_number`, `rank`,
267+
`dense_rank`, `percent_rank` or `cume_dist`, the direction `asc` or `desc`,
268+
and every `partitionBy` / `orderBy` field is quoted as a column. Anything
269+
else answers `400 INVALID_QUERY`. Fix: use one of those; send a window that
270+
takes an argument (`lag`, `lead`, `ntile`) through `execute()` with bound
271+
parameters.
256272
- **On PostgreSQL, `introspectSchema` reads each table in its own schema**, and
257273
refuses a `search_path` that reaches one table name in two schemas (`74fcec1`,
258274
#24920). Fix: point the connection at one schema.
@@ -302,6 +318,16 @@ door (`422 INVALID_METADATA`), where 18.0.0 accepted it.
302318
naming the parameter (`17f3c65`, #25260), and the published list-query
303319
schemas declare it a non-negative integer (`5f30f00`, #25300). Fix: send a
304320
whole count of zero or more, or leave it out.
321+
- **A credential is honoured only in the environment whose auth service
322+
validated it** (`1361a5a`, #25565), on every route behind the shared REST
323+
auth gate: crud, actions, import and export, batch, sharing, approvals,
324+
analytics, search and security, scoped and unscoped. Before, an environment
325+
with no `auth` service of its own fell back to the default environment's.
326+
A mismatch now answers the same `401` as an unauthenticated request. Only a
327+
multi-environment host whose per-environment kernels carry no `auth`
328+
service meets it; a single-kernel deployment is unchanged. Fix: give each
329+
environment's kernel its own `auth` service, and sign in to the environment
330+
you call.
305331
- **`GET {base}/docs`, the OpenAPI HTML viewer page, is retired**
306332
(`1b3b65e`, #25124). `GET {base}/openapi.json` stays: point your own viewer
307333
at it.
@@ -472,6 +498,12 @@ rows.
472498
`controlled_by_parent` detail follows its master. Nothing changes for a
473499
principal that does not hold the `external` grade, and no earlier release
474500
shipped it.
501+
- **`FlowSchema.runAs` now says what a `system` run may not write**
502+
(`025f6de`, #25564): its description names the refused writes to the four
503+
identity-assignment objects (above, under security), and ADR-0087 D3 records
504+
the change of meaning as `flow-run-as-system-identity-assignment-write-refused`,
505+
so `os migrate meta` lists it for a flow that wrote those rows. It carries no
506+
conversion, and every flow parses as before.
475507

476508
A change of meaning that also breaks is listed with the breaking changes above.
477509

@@ -585,7 +617,8 @@ reinstall.
585617
#### 2. Before the first boot
586618

587619
- Production `os serve` or `os start`: set `OS_AUTH_SECRET`, and do not rely on
588-
`--no-server` or `OS_MIGRATE_AND_EXIT=1` to boot without it.
620+
`--no-server` or `OS_MIGRATE_AND_EXIT=1` to boot without it. A kernel you
621+
embed needs it too, unless it boots with `NODE_ENV=development` or `test`.
589622
- A stack that mounts no auth (`auth` tier off, `--preset minimal`) fails a
590623
production boot: enable auth.
591624
- CORS: list every cross-origin front-end in `OS_CORS_ORIGIN`, loopback origins
@@ -628,3 +661,5 @@ managed datasource.
628661
assignments, invitations or memberships, and move it to `runAs: 'user'` or
629662
the organization endpoints.
630663
- Read the boot's upgrade impact report beside the access upgrade report.
664+
- On a multi-environment host, give each environment's kernel its own `auth`
665+
service before callers reach it, or they get `401`.

‎content/docs/releases/v18/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ Each release below is a self-contained page: what it changed, what breaks, and
135135
its own upgrade checklist.
136136

137137
- **[18.1.0](/docs/releases/v18/18-1)** — the first minor, and a minor that
138-
breaks: 87 of its changesets are declared breaking (ruling A grades a
138+
breaks: 90 of its changesets are declared breaking (ruling A grades a
139139
non-protocol breaking change `patch` and a protocol narrowing `minor`). Its
140140
[upgrade checklist](/docs/releases/v18/18-1#upgrade-checklist) moves an
141141
18.0.0 app to 18.1.0; it was compiled from the changesets before the cut and

‎docs/adr/0073-automation-execution-identity.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111
1212
---
1313

14+
## Amendment: a `runAs:'system'` run writes no identity assignment (2026-10-24, #25251)
15+
16+
M2's last bullet below says "`runAs:'system'` stays god-mode throughout". That no longer holds for one family of writes. Ruling 6354966423 (letter B) on #25251 carved identity assignment out of a flow's system elevation, the same way the 2026-10-03 ruling carved the stored-metadata tables out of app-authored work: a flow is app-authored automation, and `runAs` declares its authorization posture, not an identity (D2).
17+
18+
**What is refused.** An insert, update, delete, transfer, restore or purge of a row of `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member`, from a context that carries `isSystem` **and** a `flowRunId`. That is the context a `runAs:'system'` run's data nodes write under (`resolveRunDataContext`). The security middleware refuses it ahead of its `isSystem` short-circuit with `PERMISSION_DENIED` (403), and nothing is written. The refusal holds at any depth of the flow's own write: a cascade leg or a hook's write under the same context is refused the same way. Landed in #25483 (plugin-security, the run-time arm) and #25564 (the `FlowSchema.runAs` description and the ADR-0087 D3 entry `flow-run-as-system-identity-assignment-write-refused`).
19+
20+
**What is unchanged.** A system run's reads of the four objects, and its writes to every other object. A `runAs:'user'` run, which carries a `flowRunId` without `isSystem` and reaches the ADR-0090 D12 delegated-administration gate as the user it acts for. Every platform writer whose system context carries no flow run: the seeders, the publish materializer, better-auth and its reconciliation, invitation placement, and the organization-administrator grant.
21+
22+
**The remedy the refusal names.** For position holdings and permission-set grants, `runAs:'user'`: the D12 gate then judges the acting user and records that user as `granted_by`. For invitations and memberships, the organization endpoints (`invite-member`, `cancel-invitation`, `update-member-role`, `remove-member`), which apply the invitation's placement and role-cap checks.
23+
24+
So `system` stays full elevation everywhere else, and the M2 bullet is read with this one exception.
25+
26+
---
27+
1428
## Amendment: user-less data ops are now REFUSED (2026-07-28, #3760)
1529

1630
Two load-bearing claims below turned out to be wrong. Both were about *severity*, not about the model — D1–D5 stand unchanged, and D5 shipped in full.
@@ -21,7 +35,7 @@ Two load-bearing claims below turned out to be wrong. Both were about *severity*
2135

2236
**Consequently `runAs:'user'` + no trigger user now REFUSES the data operation** (`UnscopedRunDataAccessError`, thrown from `resolveRunDataContext` — the single place every data node resolves its context). This implements D5's ruling ("user-less `runAs:'user'` is a configuration error") at the only layer that can see the record-change case, since whether a triggering write carries a user is not knowable at authoring time.
2337

24-
Note what this deliberately is **not**: it does not re-badge these runs as `isSystem`. The security middleware's `isSystem` short-circuit precedes its package-managed-row / system-row / audience-anchor / delegated-admin gates, all of which a principal-less context still has to clear — so "unscoped" was never equivalent to "system", and elevating would have *widened* these runs (e.g. letting them write `sys_user_position`) rather than preserving the status quo.
38+
Note what this deliberately is **not**: it does not re-badge these runs as `isSystem`. The security middleware's `isSystem` short-circuit precedes its package-managed-row / system-row / audience-anchor / delegated-admin gates, all of which a principal-less context still has to clear — so "unscoped" was never equivalent to "system", and elevating would have *widened* these runs (e.g. letting them write `sys_user_position`) rather than preserving the status quo — a write that, since the 2026-10-24 Amendment above, a `runAs:'system'` run is itself refused.
2539

2640
This is an **interim** posture, not a replacement for D2. Refusal is strictly safer than today and forward-compatible: when the `automation` principal lands, the refusal point becomes the place that resolves it, and these runs go from *refused* to *RLS-enforced* with no third state. Ordering is unchanged — D2 still waits for a real consumer.
2741

0 commit comments

Comments
 (0)