Skip to content

Commit 1df8c63

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24901-hook-nested-quote-family
2 parents d83fca2 + 05b28c1 commit 1df8c63

13 files changed

Lines changed: 623 additions & 33 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the OAuth application register form offers only Web and Native, and its Redirect URLs help states the rule for each (#24773)
6+
7+
Clause-②: no (a form's offering: the register route still accepts every type value it accepted, and nothing on the server reads the action's param options, so no accept set or served shape moves)
8+
9+
Setup › OAuth Applications › Register offered four application types: Web, Native, User-agent based and Public. The OAuth provider judges only `web` and `native`, so the register route registers the other two as `web`, with a client secret. The form now offers Web and Native, and still defaults to Web.
10+
11+
- **Redirect URLs help.** It said "Must use https:// in production", which did not tell an admin that a local `http://localhost` callback needs the Native type. It now states the rule for each type: a Web application needs `https://` on a host other than localhost or another loopback address; a Native application may also use `http://` on exactly `localhost`, `127.0.0.1` or `[::1]`, on any port.
12+
- **Application Type help.** New: choose Native for a desktop or mobile app, or for an app in local development that redirects to `http://localhost`.
13+
- **Unchanged.** The register route accepts the same values as before, including `user-agent-based` and `public`. The stored `sys_oauth_application.type` column still lists all four values, so a row written under an earlier provider line keeps its value. The `public` column is unchanged.
14+
15+
The zh-CN, ja-JP and es-ES translations carry the new help texts.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): a `controlled_by_parent` detail write asks its master the object-level question the master's own write door asks (ADR-0055 §3)
6+
7+
Clause-②: no
8+
9+
ADR-0055 §3 says editing, deleting or creating a detail requires edit access to its master. The master-edit gate's object-level leg asked only for an object-level `update` grant on the master. It now asks what the master's own write door asks of an `update` on the master, with that door's inputs: the master's security posture must resolve, the capabilities the master requires for `update` (ADR-0066 D3) must be held, and the grant is asked under the master's posture (ADR-0066 D2). A principal the master's own write door refuses at its object gate can no longer write a detail under that master.
10+
11+
- **Every door that asks the leg.** An insert, a by-id update and a by-id delete of a detail answer `PERMISSION_DENIED` (403) and write nothing. `checkControlledByParentWrite`, which the comment and attachment parent gates ask, answers `deny` on the `object_permission` leg. A record-grained `security/explain` of a by-id write follows the same leg.
12+
- **An insert answers before the reference check.** For such a principal, an insert under a master id that exists answers the same refusal as one under an id that exists nowhere. A principal that also cannot read the master, on an on-behalf-of write too, now gets this `PERMISSION_DENIED` on insert where it got the reference check's `VALIDATION_FAILED`. An insert under a master object that does not resolve answers `PERMISSION_DENIED` where it answered `OBJECT_NOT_FOUND` (404).
13+
- **Unchanged.** A principal the master's own write door admits for an `update` keeps writing its details, and so do a super-user grant and a system context. Under a master that is `public`, resolves and requires no capability for `update`, every principal gets the answer it got before.

‎.claude/agents/os-dev.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ model: opus
8585
- `bash scripts/pm/os-verify-lock.sh -c '<command>'`(或 `-- <argv>`);⛔ 永不手搓 `flock`/lockfile。
8686
- 一次前台调用领全部等待预算,阻塞到拿锁或 99;⛔ 不轮询重试,恒设 `OS_VERIFY_LOCK_SLOT`。
8787
- 它不保证机器空闲:`check:*` 门禁、install、dev server 不走它,与持锁同核并跑。
88-
- 测试电池 `pnpm check:pm-dispatch-gates` 亦属 `check:*`:不走锁,`nohup … > LOG 2>&1 &` 后 `tail --pid`。
88+
- 超前台上限者(`check:pm-dispatch-gates`、全量 dogfood):`nohup … > LOG 2>&1 &` 后 `tail --pid`。
8989
- 锁下墙钟绝对值是共享盒读数;只包命令本身,不包你的阅读与判断。
9090
- 结论读它印的 `VERDICT command-exit` 行,⛔ 不读裸 `$?`;按到达序授予,排队是常态,不是挂死。
9191
2. **压住堆**:重命令前缀 `NODE_OPTIONS=--max-old-space-size=4096`,要抬需给理由。
@@ -172,13 +172,11 @@ model: opus
172172
- `set -o pipefail` 与 `${PIPESTATUS[0]}` 仅当下游读到 EOF 才安全。
173173
- `| head -N` 读满即关读端,生产者吃 SIGPIPE 以 0 退出:是管道改了生产者的退出码。
174174
- 此陷阱不限于门禁:任何 `cmd | head` 之后读退出码都中招(`git grep`、`node` 皆然)。
175-
- 陷阱在 shell 用法里,不可机械化;⛔ 别等机械强制。
176175
- 两类跑了却没测到,都读作 NOT MEASURED,不读作绿也不读作红。
177176
- 证据已销毁(评论、卡或 PR 答 404)的复核项记 NOT MEASURED 并写因,⛔ 不记通过或「无旗」。
178177
- ① 包的 `typecheck` 可能 `exclude` 掉 `**/*.test.ts`;声称它覆盖你的测试前,用 `--listFiles` 数。
179178
- ② `MODULE_NOT_FOUND` 一类的 exit 1 不是红门禁:多半是脚本名或路径敲错,根本没进到门禁体。
180179
- 它与 `exit 99` / queue-timeout / `PREREQUISITE NOT MET` 同类:用真正的 `pnpm check:*` 命令重跑。
181-
- ⛔ 不把这类结果写进报告当作一次失败的测量。
182180

183181
## 标准条款住在这里,不住在你的派发词里
184182

@@ -249,7 +247,6 @@ model: opus
249247
- `packages/qa/dogfood` 是一例,不是定义;读成 dogfood 专属,普通单元套件里的 ablation 会被当真。
250248
- 两个方向不对称:没 build 的修复是会被注意到的假红;没 build 的 ablation 保持绿。
251249
- 那份绿给一条可能永远失败不了的断言背书,对 CI 永久隐形(CI 构建正确,那边永远绿)。
252-
- 消融本就为证明新门禁能失败时,这份假绿读作门禁没触发,于是有人去弱化一条好门禁。
253250
- 每一腿(变异与还原)都是:改动 → 证明它真落到了磁盘 → `pnpm --filter <pkg> build`。
254251
- 落盘走 `node scripts/ablation-replace.mjs`,不走 `-i` 家族:锚点必须命中,写入与还原按磁盘核验。
255252
- 再证明它到达了 `dist/`,才读运行结果:`node scripts/ablation-dist-preflight.mjs <pkg> '<marker>'`。
@@ -276,7 +273,10 @@ model: opus
276273
- 分区 pin `test/vitest-tiers-partition.test.ts` 在某个测试文件两层皆无或皆有时变红。
277274
- 用 `git push -u origin claude/issue-<n>-<slug>` 推上去,网络失败退避重试;pre-push 拒卡片 trailer。
278275
- Draft PR 指向 `main`,正文首行 `Fixes #<n>`;合并不应关卡时用 `Part of #<n>`,并说明留下哪一半。
279-
- PR 开出后合 main 只因冲突、main 致 CI 红、出队,后只跑 §10 复检;dogfood 只在终 head 跑一次。
276+
- PR 开出后合 main 只因冲突、main 致 CI 红、出队,后只跑 §10 复检。
277+
- 全量 dogfood 只在终 head 跑一次,仅当标题带 `!` 或 `Clause-②:` 带 `(narrowing)` 臂(yes/no 皆算)。
278+
- 命令同 CI:按 ① 建闭包,锁下 `pnpm --filter @objectstack/dogfood test`;⛔ 无 `OS_TEST_SHARD`、无路径。
279+
- 红 = 本改动的后果(陈旧 fixture)同 PR 修,余照报;⛔ 永不跳过或收窄;计数记 `dogfood_battery`。
280280
- 认领带 `Clause-②:` 行才照抄进正文行首:`Check Changeset` 读正文不读卡;无则零写,⛔ 不自造。
281281
- ⛔ 永不 `Fixes` 一张还在决策箱的卡:合并会静默关掉它,而收件箱过滤只读 open。
282282
- ⛔ 不写否定式的关单句,它照样关掉点名的卡:解析器无视否定,只匹配关键词 + `#<n>`。
@@ -370,6 +370,7 @@ model: opus
370370
"summary": "what was implemented, 2-4 sentences",
371371
"tests": "commands run + pass/fail evidence (real output excerpts); ablation only where a tested behaviour changed: rebuild + on-disk mutation proof",
372372
"gates": "local subset: each command + exit code captured before any pipe + --ran reconciliation; every family CI runs: the PR's check-runs (name + conclusion) cited, not re-run locally",
373+
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm | <head sha> · the lock's VERDICT line · Test Files N/N · Tests M/M · every failure named",
373374
"mcp_calls": "<n> — MCP GitHub calls with tool names; a write tool in the list = this report is refused",
374375
"api_writes": "<n> — REST proxy writes, each endpoint listed",
375376
"open_questions": [
@@ -382,7 +383,6 @@ model: opus
382383
- `status: "rework"` = 你自知不完整的部分成果,在 `summary` 里说明为什么。
383384
- `premise_still_valid: false` = 核验证伪了前提(规则 6):证据进 `summary`,`pr` null 或只圈存活部分。
384385
- 报告模板是工具,不是真相:某字段的预设与实际发生的对不上时直说。
385-
- 例:反向验证方向反转、前提死了、某个产物在此无意义;按模板硬造比留白更糟。
386386

387387
## 字节与 sanitizer 纪律
388388

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
- 切 `Part of` 的正文最易再武装自动关闭:否定句、记账句、引号包裹,解析器一律照关。
1010
- 安全拼法 = 卡号旁零动词,关闭安排写进评论。
1111
- 闭合关键词两读:翻 ready 前亲核首行、亲扫全正文,⛔ 关键词永不挨另一张 open 卡编号。
12-
- 这是合并前唯一的人工关口,⛔ 不只信报告;合并后那一读见 `landing-operations.md`。
1312
- `Part of` 收口的卡不会自动关,`pm:dispatched` 必须手工摘,动作要件见落地细则 B。
1413
- ACCEPT 那一刻记进落地待办,⛔ 不留给下次巡检。
1514

@@ -48,6 +47,7 @@
4847
- ⛔ 非当前 head 上的 `cancelled` 零动作、永不重跑:新推送自带全套 run。
4948
- 席位核对门禁证据 = 钉 head 的 check-runs(名+结论);本地并集只证本地子集,⛔ 不替缺席者。
5049
- dev 本地并集钉 head:所抄 `git rev-parse --short HEAD` 不符 PR `head.sha` 或没抄 ⇒ 双向都不入账。
50+
- `!` 或 `(narrowing)`:入队前核 `dogfood_battery` 钉 head、全量、红已修/已报;缺或收窄 ⇒ REWORK。
5151

5252
## 断言与收益
5353

‎docs/adr/0123-no-active-organization-session-semantics.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# ADR-0123: An authenticated session with no active organization is a legal state — reads resolve to nothing, tenant-scoped writes are refused loudly
22

33
**Status**: Accepted (2026-08-13)
4+
> **Amendment (2026-10-20) — D2's refusal also answers two inserts its scope paragraph did not name, and the "What does not change" line no longer lists platform operators whole (#24823; PR #24819, `8a058d85a`, and PR #24868, `98c3d00447`).** Both arms are D2's own refusal — `403 PERMISSION_DENIED`, naming the missing active organization (D4) — thrown from the one place it is spelled (`packages/plugins/plugin-security/src/security-plugin.ts#refuseNoActiveOrganization`, step 3.7). Neither changes an outcome: each insert was already refused by the engine's organization check (ADR-0131 D9) as an organization-less system write, answered `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` with its prose withheld from the wire. Only the answer moved, to the one this record gives the state.
5+
>
6+
> 1. *A union-scope insert with no active organization (PR #24819).* Under `group`, the scope pre-test reads the membership set, so a member whose session has no active organization passes it — rightly, for reads, and for updates and deletes of rows the membership wall selected, which keep the organization they already have. An insert needs somewhere new to land, and a new row lands in the active organization: the engine stamps it from `tenantId` (ADR-0105 D5). With none, the insert is refused, as `isolated` already refused the same session.
7+
> 2. *A platform operator crossing the wall (PR #24868).* A platform operator crossing the wall on a posture-permitting object (ADR-0095 D3) has no Layer 0 there, so neither the pre-test nor arm 1 reaches it, under `isolated` or `group`. Crossing lets it read every organization's rows and update them where they are; it gives a new row no organization. From a session with no active organization, its insert is refused. ADR-0131 D9 refused that write already: "Nothing defaults to any owner other than the derivable one."
8+
>
9+
> Both arms judge the insert's stored image — the rows after the `beforeInsert` chain, the ones the engine's check reads next — so they refuse exactly the rows that met the 500. A stored row that names an organization (a hook placed it) is not refused by either arm and is judged as before; one row that names none refuses the whole write. Reads, updates of existing rows, deletes, inserts from a session with an active organization, and system contexts are unchanged. D2's scope paragraph is kept as written, with a pointer here: its operator clause and its "empty membership set" sentence still bound the scope pre-test. The **What does not change** line is corrected in place.
10+
411
**Deciders**: ObjectStack Protocol Architects (maintainer ruling on [#8247](https://github.com/objectstack-ai/objectstack/issues/8247), 2026-08-13)
512
**Builds on**: [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) (D1 — Layer 0 is an independent, always-first, AND-composed tenant filter; W1/W2), [ADR-0105](./0105-group-tenancy-posture-and-first-class-org-scope.md) (D1/D2 — the posture spectrum and the `group` union wall; "empty/absent scope → deny"), [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (D1/D2 — the membership reconciler as the single owner of the "every user gets a membership" invariant, and the `session.create.before` hook that resolves `activeOrganizationId` from `sys_member`), [ADR-0112](./0112-error-code-vocabulary-and-ledger.md) (D3 — the closed error-code vocabulary this refusal draws from rather than extending), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove — a declared wall the write path does not meet is the defect this record closes)
613
**Consumers**: `@objectstack/plugin-security` (`security-plugin.ts` step 3.7, `tenant-layer.ts`), `@objectstack/plugin-auth` (`auth-manager.ts` `composeDatabaseHooks`, `reconcile-membership.ts`), `@objectstack/plugin-sharing` (`sharing-rule.ts` — already conforming), `@objectstack/plugin-audit` (`auth-session-audit.ts` login/logout rows)
@@ -57,7 +64,7 @@ So both halves were individually defensible and jointly produced a write that su
5764
- *Writes that place or move a row* — `insert` and `update` — are **refused**, with `PERMISSION_DENIED` / HTTP 403 and a message that **names the missing active organization**. The code comes from ADR-0112's standard catalog rather than a new registration: the condition is a permission-class refusal, and ADR-0112 D3 directs a generic condition to the catalog instead of a synonym. This is the same code and status PR #8237 put on the wire for the same state, which is what makes "#8158 conforms" a measured statement rather than an assertion.
5865
- *`delete`* places nothing and decides no tenant. Its target is selected through the Layer 0 row wall, which already resolves to nothing — so a delete under this state matches no row and is governed by the read rule, deliberately. This boundary is stated so the next author does not read its absence as an oversight.
5966

60-
**The scope of the refusal is exactly the state, and no wider.** It fires only for an authenticated, non-system caller, on an object the posture actually walls (a tenant object under `isolated`/`group`; never a `tenancy.enabled:false` platform-global object, never an object with no `organization_id` column, never a federated phantom anchor), who is not a platform operator crossing the wall by ADR-0095 D3. System contexts — boot seeding, reconcile hooks, backfills, imports — short-circuit the security middleware entirely and are untouched. Under `group`, "no organization scope" means an **empty membership set**, matching ADR-0105 D2's own fail-closed rule.
67+
**The scope of the refusal is exactly the state, and no wider.** It fires only for an authenticated, non-system caller, on an object the posture actually walls (a tenant object under `isolated`/`group`; never a `tenancy.enabled:false` platform-global object, never an object with no `organization_id` column, never a federated phantom anchor), who is not a platform operator crossing the wall by ADR-0095 D3. System contexts — boot seeding, reconcile hooks, backfills, imports — short-circuit the security middleware entirely and are untouched. Under `group`, "no organization scope" means an **empty membership set**, matching ADR-0105 D2's own fail-closed rule. *(Amended 2026-10-20 — the operator clause and the "empty membership set" sentence bound the scope pre-test, not every write. Two inserts from a session with no active organization, of a row that names no organization, are refused past them: a `group` member's with a non-empty membership set, and a platform operator's crossing the wall. See the amendment in the status block above.)*
6168

6269
**D3 — ⛔ No silent NULL stamping, and the ledger carve-out is paid for by ordering.** Under the state this record governs — an authenticated caller with **no active organization** — no path may write a tenant-scoped row with a NULL tenant and report success, in any subsystem; D2's refusal is what makes that hold, and no layer gets an exemption from it.
6370

@@ -75,7 +82,7 @@ The reconciler is idempotent, yields to any pre-existing membership, and never t
7582

7683
**What changes.** An authenticated caller with no active organization, writing to a walled tenant object, now gets `403 PERMISSION_DENIED` naming the missing organization where they previously got `2xx` and an unreadable row. This is a **wire-visible behaviour change** on a security boundary; it is breaking in the direction of refusing something that used to silently corrupt.
7784

78-
**What does not change.** Reads. System contexts. Platform operators. Single-posture deployments (no wall, Layer 0 inert). Objects that opted out of tenancy. The supplied-`organization_id` forge guard, which keeps its own semantics and its own message. #8158's landed fix.
85+
**What does not change.** Reads. System contexts. ~~Platform operators.~~ Platform operators' reads, their updates of existing rows, and their inserts from a session with an active organization. *(Corrected by the 2026-10-20 amendment — a crossing operator's insert from a session with no active organization, of a row that names no organization, now gets D2's 403 where the engine answered 500; see the status block above.)* Single-posture deployments (no wall, Layer 0 inert). Objects that opted out of tenancy. The supplied-`organization_id` forge guard, which keeps its own semantics and its own message. #8158's landed fix.
7986

8087
**Dispositions this record carries.** #8208 becomes the D2 refusal — the silent NULL stamp is unreachable. #8245 becomes the D3 ordering fix — the first session mints with its organization, so its ledger rows carry a tenant.
8188

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,15 +1530,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
15301530
},
15311531
redirectURLs: {
15321532
label: "Redirect URLs",
1533-
helpText: "One URL per line. Must use https:// in production."
1533+
helpText: "One URL per line. Web applications need https:// on a host other than localhost or another loopback address. Native applications may also use http:// on exactly localhost, 127.0.0.1 or [::1], on any port."
15341534
},
15351535
type: {
15361536
label: "Application Type",
1537+
helpText: "Choose Native for a desktop or mobile app, or for an app in local development that redirects to http://localhost.",
15371538
options: {
15381539
web: "Web",
1539-
native: "Native",
1540-
"user-agent-based": "User-agent based",
1541-
public: "Public"
1540+
native: "Native"
15421541
}
15431542
}
15441543
},

0 commit comments

Comments
 (0)