Skip to content

Commit 6bf9207

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17027-boot-error-scan
2 parents 9e749a1 + 0780e88 commit 6bf9207

19 files changed

Lines changed: 1548 additions & 78 deletions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a refused write reports at `warn`, not `error` — the caller was already told (#17052)
6+
7+
`insert`, `update` and `delete` each end their `catch` with `throw e`, then
8+
logged the failure at ERROR one statement earlier. AGENTS.md → *Degradation log
9+
levels* names that exact shape and forbids it: "a failure handed to the CALLER
10+
is not a degradation at all … Do not bolt a `logger.error` onto such a site."
11+
12+
**This moves published behaviour**, which is why it is a changeset rather than a
13+
`skip-changeset`: the level is what an operator greps, and at least one consumer
14+
reads it structurally. `scripts/publish-smoke.sh` fails a boot on any
15+
error-level line (`SMOKE_ERROR_LOG_PATTERN`), and that is how the defect was
16+
found — `@better-auth/oauth-provider` seeds `sys_oauth_resource` in `insertOnly`
17+
mode and documents its `identifier` UNIQUE constraint AS its race-safety
18+
mechanism, catching the collision and continuing at `debug`. Our line was
19+
emitted before that catch ever ran, so a healthy first boot of every fresh
20+
`create-objectstack` project printed `ERROR Insert operation failed` and red-lit
21+
`publish-smoke / packed-tarballs` for six consecutive runs on a candidate whose
22+
auth and CRUD probes were all green.
23+
24+
**Nothing else about the entry moved.** Same message, same `object` meta, same
25+
redaction (#8682: the bound statement and its values stay cut from `message`
26+
and `stack`), same subject (#14095: the entry carries the driver's own error —
27+
a `DuplicateRecordError`'s `cause` — never the envelope, so the failing column,
28+
MySQL's index name and the driver's frames survive). The `Logger` contract gives
29+
an `Error` slot to `error`/`fatal` only, so the engine now builds the
30+
`{ error: { message, stack } }` bag that slot used to build; handing the Error
31+
to `warn` as meta would have serialised `{}`, because those two fields are
32+
non-enumerable. The rendered line is byte-identical apart from the level word,
33+
and that equivalence is pinned rather than asserted.
34+
35+
If you grep your logs for these three messages, keep the message and drop the
36+
level from the pattern. If you alert on error-level lines from `@objectstack/objectql`,
37+
a refused write no longer raises one — the write's exception still does.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
"@objectstack/plugin-security": patch
3+
---
4+
5+
fix(security): resolve `current_user.accessible_org_ids` into the RLS variable bag (#16518)
6+
7+
`patch` — a bug fix in a released package. No API signature changes, no exported
8+
symbol added, no spec or ADR edit: the contract already promised this, and only
9+
the line that delivers it was missing.
10+
11+
## What was wrong
12+
13+
`packages/spec/src/contracts/rls-membership-resolver.ts` does not merely reserve
14+
the name `accessible_org_ids`. It declares the field's SHAPE (`:53`,
15+
`accessible_org_ids?: string[]`), states at `:35` that the key is CORE-resolved
16+
and not an app resolver, and lists it at `:70` in
17+
`RESERVED_RLS_MEMBERSHIP_KEYS` — so an app's membership resolver is refused when
18+
it tries to supply the set itself. `ExecutionContext.accessible_org_ids` goes
19+
further and names the RLS spelling outright: *"RLS policies may reference it as
20+
`organization_id IN (current_user.accessible_org_ids)`"*.
21+
22+
`RLSUserContext` declared `id`, `organization_id`, `positions`, `org_user_ids`
23+
and `email`, and nothing copied `accessible_org_ids` out of the execution
24+
context. So the key was reserved on the grounds that core resolves it, and core
25+
did not resolve it — a slot with a declared shape and no filler, which is the
26+
ADR-0049 "declared but unenforced" shape.
27+
28+
**The cost is the invisible one.** A predicate such as
29+
`employer_org IN (current_user.accessible_org_ids)` compiled to an unresolved
30+
variable, every applicable policy dropped out, and `RLS_DENY_FILTER` returned
31+
**zero rows with no error raised**. Nothing failed. An empty list is
32+
indistinguishable from "this user really has no data", which is how the shape
33+
survived three green static gates and, in the reporting app, left ten policies
34+
across six objects inert — the entire multi-tenant isolation model.
35+
36+
The failure direction is **closed**: zero rows, never a cross-tenant read. This
37+
is a usability and declared-means-enforced defect on a security surface, not a
38+
leak.
39+
40+
## What it does now
41+
42+
`RLSCompiler.compileFilter` copies `ExecutionContext.accessible_org_ids` into
43+
`RLSUserContext`, following `org_user_ids`' precedent exactly — both are
44+
core-resolved membership sets the runtime **pre-resolves**, precisely so this
45+
compiler never has to issue a subquery. The compiler is unchanged otherwise; it
46+
already handled the value correctly once present.
47+
48+
The producer already existed and is unconditional: `resolve-authz-context.ts`
49+
types the set as required and `assemble-execution-context.ts` copies it on every
50+
face, in every posture (*"in `single` posture the set is resolved but no wall
51+
consumes it"*). Only the consuming line was missing.
52+
53+
One consequence worth naming: **reserved now means reserved at the compiler
54+
too.** `stageRlsMembership` screens reserved keys out of a *resolver's* answer,
55+
but a bag already present on the context was spread through unscreened, and
56+
landed in the variable bag because nothing named the field. Now that the kernel
57+
names it, the compiler's own "a membership key never clobbers a named field"
58+
rule covers it and the kernel's value wins.
59+
60+
## Measured, end to end
61+
62+
A rig on real drivers (`driver-sql`, `driver-sqlite-wasm`), six rows across
63+
three organizations, a caller holding membership in two of them:
64+
65+
| predicate | before | after |
66+
|:--|--:|--:|
67+
| `employer_org IN (current_user.accessible_org_ids)` | **0 of 6** | **4 of 6** — the rows of both orgs |
68+
| same, caller scoped to ONE org | 0 of 6 | 2 of 6 — that org only |
69+
| same, caller with no set / an empty set / an org with no rows | 0 of 6 | 0 of 6 — unchanged, still fails closed |
70+
| a predicate naming a NON-EXISTENT variable | 0 of 6 | 0 of 6 — unchanged (#16119's face, untouched) |
71+
| `org_user_ids`, `organization_id`, `email`, `id`, an app membership key || byte-identical |
72+
73+
An app **could** work around the defect by supplying the same set under its own
74+
unreserved key through `rlsMembership` and rewriting its predicates to
75+
`current_user.my_org_ids`; that reads 4 of 6 on the same rig, before and after.
76+
The workaround costs every app a membership-resolver registration it should not
77+
need and moves every predicate off the documented spelling — and it is no longer
78+
necessary.

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
108108
| `needs-user-decision` | 决定待做:永不派发、除代裁通道外永不代答;维护者的收件箱 |
109109
| `pm:on-hold` | 决定已做且答案是暂不做:不派发不催;仅当带机器可读 `Restart-when:` 行才合法 |
110110
| `pm:blocked` + 正文行 `Blocked-by: #N` | 等上游:选择期跳过,#N 关闭时由解锁扫描放回;工已完、PR 被外部门禁卡住的同用本态 |
111-
| `pm:awaiting-maintainer` | 决定已做,只剩一次 GitHub 之外的人工动作:不派发不催;与其它 pm 状态标签互斥 |
111+
| `pm:awaiting-maintainer` | 决定已做,只剩一次 GitHub 之外的人工动作:不派发不催;与其它 pm 状态标签互斥;入态恒带行首 `Maintainer-action:` 行(细则见 `references/state-machine.md`),无行即半态 |
112112
| `pm:blocking` | 有 open 下游依赖者(自 `Blocked-by:` 索引推导的缓存,⛔ 不手工挂);进选择全序 |
113113
| `pm:retriage` | 向分诊提问(改判、跨域 PR 指定车道、改路由、拆卡、裁 dev 报告留下的分叉),异议评论写明所求;与现行 `pm:*` 并存、⛔ 不摘原标;带本标签的 `pm:queue` 卡跳过派发 |
114114
| `finding` | 立卡三类内待首次定级,定级即离标;三类外关 not planned;不占队列不进收件箱 |
@@ -128,7 +128,6 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
128128
- 无机制可唤醒的卡 ⛔ 不 hold:关 not planned,理由/出处载关单评论;重开免费,维护者可否决。
129129
- 缺陷卡 ⛔ 不藏进 hold 也不自行关闭:可复现且用户可达 ⇒ 回 `pm:queue`
130130
- declared≠enforced 观察类 ⇒ 转 enforce-or-remove 通道;真 won't-fix 候选 ⇒ 逐卡进决策箱。
131-
- 机会主义重启条件必须点名触发文件。
132131
- 派发与折叠检查时读半状态巡查锚的 H17 触发文件索引,与本次派发文件面求交。
133132
- 相交 ⇒ 按该 hold 评论的 rider/restart 条款处置:点名该单,顺手活列为申报过的增项。
134133
- 关闭即在同一笔摘掉 `pm:*` 状态标;`domain:*` 与类型标签留下,归属不是状态。
@@ -245,11 +244,11 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
245244
|:--|:--|
246245
| `domain:engine` | `packages/objectql``packages/core``packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`;`packages/metadata*``packages/platform-objects`;`packages/drivers/driver-*`;退役标签 `domain:engine-core` / `domain:metadata` / `domain:drivers` 只退出流通,GitHub 标签对象保留 |
247246
| `domain:services` | `packages/services/*``packages/connectors/*``packages/triggers/*``plugin-approvals``plugin-webhooks``plugin-email``plugin-reports``embedder-openai``knowledge-*`;`plugin-auth``plugin-security``plugin-sharing``plugin-audit`;退役标签 `domain:identity` 同上只退流通 |
248-
| `domain:devx` | `packages/lint`(与 spec 相交的面均按锚定规则的例外切分)、`packages/sdui-parser``content/docs/**``apps/docs``scripts/`(门禁类;与 `domain:skills` 的分界按门禁的 SUBJECT:治理 agent 指令面/governed 面的归 skills,治理代码/文档质量的归本域) |
249-
| `domain:skills` | `.claude/skills/**`(含本文件)+ `skills/**`;根 `AGENTS.md` + 根 `CLAUDE.md`;governed 面的治理执行文件:`.github/CODEOWNERS` + SUBJECT 是 governed 面本身的门禁/审计(现为 `scripts/pm/check-governed-merges.mjs`) |
247+
| `domain:devx` | `packages/sdui-parser``content/docs/**``apps/docs`;`packages/lint``scripts/`(门禁类)、`.github/workflows/`(门禁接线);三者与 spec 相交的面按锚定规则的例外归 `domain:spec`,门禁再按 SUBJECT:治理 agent 指令面/governed 面的归 skills,治理代码/文档质量的归本域 |
248+
| `domain:skills` | governed 面全量(含本文件;统一定义见「governed 面统一定义」行);governed 面的治理执行文件:`.github/CODEOWNERS` + SUBJECT 是 governed 面本身的门禁/审计(现为 `scripts/pm/check-governed-merges.mjs`) |
250249
| `domain:spec` | `packages/spec` 整包:schema 形状、`contracts/**`、退役行为半边、strictness 台账;describe/JSDoc/墓碑散文/错误 guidance 与 alias 表;`packages/spec/scripts/**``packages/spec/docs/**` 及按锚定规则的例外归本域的工具链(域边界枚举与席内分派见 `references/lanes/spec.md`) |
251-
| `domain:cli` | `packages/cli``runtime``verify``qa``types``packages/rest``packages/mcp``packages/observability``packages/client*``cloud-connection``create-objectstack``packages/adapters/*``plugin-hono-server``plugin-dev` |
252-
| (无固定归属,按落点分诊) | `packages/apps/*``packages/console`(dist 由脚本生成 ⛔ 不手改;UI 缺陷走 `repo:objectui`)、`examples/*`(归它演练的子系统) |
250+
| `domain:cli` | `packages/cli``runtime``verify``packages/qa``types``packages/rest``packages/mcp``packages/observability``packages/client*``cloud-connection``create-objectstack``packages/adapters/*``plugin-hono-server``plugin-dev` |
251+
| (无固定归属,按落点分诊) | `packages/apps/*``packages/console`(dist 由脚本生成 ⛔ 不手改;UI 缺陷走 `repo:objectui`)、`examples/*`(归它演练的子系统)`docs/audits/**` |
253252

254253
- 表未覆盖的包首次分诊时归类并走 PR 更新本表;新增或退役 `domain:*` 必须同批改本表。
255254
- 座位在编情况以 `label:pm:seat` 索引为准,每个 `domain:*``repo:*` 恰一张座位贴。
@@ -625,7 +624,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
625624
- ④ 轮次报告单列 awaiting a human merge。
626625
- 已入队才读到本条 ⇒ 转 draft 与 disable 都做;出队以阳性探针答,ref 缺席只旁证。
627626
- skills 车道自有 PR 再按 diff 内容分流:diff 含任一 `.md` 文件 ⇒ 终局四件套照旧。
628-
- 纯代码面(`scripts/pm/` 工具`.claude/` hooks/workflows/settings、非 md 产物)⇒ skills 席自审。
627+
- 纯代码面(`scripts/pm/``.claude/` hooks/workflows/settings、非 md 产物)⇒ 复核席为 skills 席自审。
629628
- skills 席自审按契约复审档、清单不减,然后直接落地(ready → 入队),⛔ 不推维护者。
630629
- 路径面干净的才转 ready → 入队;队列是唯一被认可的落地路径,⛔ 永不队列外合并。
631630
- 入队资格 = PR 上每一个 check 全绿,⛔ 不是 required 子集;required 集是队列强制的地板。
@@ -708,10 +707,11 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
708707

709708
## 升级与决策
710709

711-
- 先过升级门槛:明显的问题直接修,大多数感觉像决定的不是决定。
712710
- 只在至少一条成立时升级:选项在产品语义或公开契约形状上真实分歧且既有规范定不了。
713711
- 或修复需破坏性/难回滚动作;其余归 PM 裁量:裁定、派发、维护者否决窗口而非许可门。
714712
- 具名不升级类(立即行动):恢复不变量;技术任务间顺序与依赖;验证策略;说明书脱节。
713+
- 四类记账事同属不升级类,恒以无产品可见行为变化为界:去重与卡片合并;台账与记账整理;
714+
- 纯文档措辞更正(无契约声明改动);门禁内部参数与扫描器盲区修复(加强,非削弱)。
715715
- dev 的 `needs_decision` 经 PM 复核落进不升级类的,PM 直接答复不上传。
716716
- 第三档:带前提的裁决,三件套缺一不可:① 裁决(选定路线)。
717717
- ② 把裁决挂在具名、可证伪的前提上,派发令要求 dev 先验前提再动手。

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
- 该命令 0 = 双肢一致且无放宽 tell,4 = 任一不成立,3 = 环境答不了;⛔ 3 不作干净。
4545
- 放宽 tell 由 `scripts/pm/check-widening-tells.mjs` 判,`no` 撞新键/成员/导出/登记即拒,附 file:line
4646
- ③ PR 全部 check 全绿,⛔ 非 required 子集;受管面不适用,draft-only 终局不变。
47-
- 外部评审链是可选事后审计:分诊定时轮与总监席召唤 ⛔ 不是放行必要条件。
4847

4948
## 降档保险丝(机读)
5049

@@ -54,6 +53,7 @@
5453
- 保险丝只测座位自会话:`mode:subagent``get_session` 量的是派发会话,⛔ 不作互证。
5554
- 传参只是配置 ⛔ 不作达档读数;条款②的 `mode:subagent` 派发恒保留标至席内复核完成。
5655
- 转录档位核验:采信或清标前 grep 子代理 transcript 中 harness 逐消息盖章的 `model` 字段。
56+
- 施工档只取 harness `model` 盖章或认领 Container & model 行;`Co-Authored-By` = 署名常量 ⛔ 非证据。
5757
- 产出裁决的每轮都须读到契约复审档位,见回退证据 ⇒ 裁决整体作废。
5858
- 父会话只有两个合法动作:逐字采纳,或整体作废(核验失败、越范围、格式不完整)。
5959
- ⛔ 永不改写、删节或润色子代理裁决。

.claude/skills/pm-dispatch/references/core-rules.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@
2727
- 一座位一车道双射,每个域恰好一个 PM;与 AGENTS.md 冲突时以 AGENTS.md 为准。
2828
## 状态模型
2929
- 可派发等于 open 加队列标签加无 assignee 且不带 `pm:retriage`,已设 assignee 即 ⛔ 永不碰。
30-
- `pm:dispatched` 与摘 `pm:queue` 恒在同一次标签写入里成对落地。
3130
- `pm:on-hold` 仅当带机器可读 `Restart-when:` 行才合法,触发文件走 `Restart-touch:` 行。
3231
- hold 放行须双查:只放最近一次转换评论的条件,其后有更新的 merged PR 即拒。
3332
- 没有可点火出口的卡 ⛔ 不 hold,直接关 not planned 并把理由与出处写进关单评论。
3433
- 缺陷卡 ⛔ 不可自行关闭或藏进 hold:可复现回队列、未兑现走 enforce-or-remove、余下进箱。
3534
- `pm:blocked` 配正文行 `Blocked-by:`,工已完而卡在门禁的同用此态并写 `Unlock-action:` 行。
36-
- `pm:awaiting-maintainer` 只剩站外人工动作;`pm:blocking` 是反向索引推导的缓存 ⛔ 不手工挂。
35+
- `pm:awaiting-maintainer` 只剩站外人工动作,恒带 `Maintainer-action:`;`pm:blocking` ⛔ 不手工挂。
3736
- `pm:retriage` 并存 ⛔ 不摘原标且跳过派发;`finding` 恒等于待首次定级、定级即离标。
3837
- 插队标签可超 `batch` 立即派发,⛔ 不豁免同文件串行、深度等待与认领协议。
3938
- 五个 pm 状态标签加 `needs-user-decision` 共六态互斥,转换恒一笔 replace,队列卡逾三天欠转换。
@@ -135,6 +134,7 @@
135134
- 升级只有两条门槛:公开契约或产品语义真实分歧且规范定不了,或动作破坏性难回滚。
136135
- 其余都是 PM 的裁量,给维护者的是否决窗口而不是许可门。
137136
- 具名不升级类:恢复不变量的修复、技术任务之间的顺序与依赖、验证策略、说明书脱节。
137+
- 记账事不升级(无产品可见行为变化):去重并卡、台账整理、纯文档措辞、门禁盲区加强。
138138
- 带前提的裁决三件缺一不可:选路线、挂具名可证伪前提、前提不成立必须报分叉。
139139
- 同族近似单默认并入既有拒收集;两个实现不一致时带治理的一侧胜出并删另一侧。
140140
- 落卡先刷新前提,每条前提行自带一条 re-check 命令;决策默认锚在所属 issue。
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# 车道岗位说明:domain:ui
2+
3+
见 SKILL.md 〈多仓协调〉;本文是本席的岗位说明,现值状态恒在座位贴,⛔ 不迁入本文件。
4+
5+
## 形态
6+
7+
- 本席为姊妹仓执行座位:在 objectui 仓认领、派发、复核、落地。
8+
- ⛔ 不产 `domain:*`、type 与定级,中央分诊唯一生产,见 SKILL.md 多仓协调规则 1 与 4。
9+
- 误标 ⛔ 不自改:挂 `pm:retriage` 加异议评论同笔,见同节规则 4。
10+
11+
## 范围
12+
13+
- 三流分流的残余流归本席:发布库 `@object-ui/*``apps/*`,判据见 SKILL.md 〈多仓协调〉。
14+
- objectui 的 `domain:devx``domain:spec` 卡归各自跨仓车道,⛔ 不归本席。
15+
- 症状位置不改流向,docs 随所记录的面走,两条同上节,⛔ 不在此另抄。
16+
- 落点不明留分诊首触,⛔ 不猜、不代判。
17+
18+
## 常设承诺
19+
20+
- 构建产物经 `pnpm objectui:refresh` 回流,见 SKILL.md 〈多仓协调〉。
21+
- 回流后的 console pin bump 是 objectstack 的单张卡,⛔ 永不做本仓 PR 的 rider。
22+
- 凡触 `packages/spec` 一律转 `domain:spec` 座位,见 SKILL.md 〈多仓协调〉。
23+
- `scripts/pm/**` 单写手恒在 objectstack 侧 ⇒ 本席的工具需求走上游卡带 `Blocked-by:` 回链。
24+
- 受管面五项含仓根发布 `skills/**`,清单、判据与禁令见 objectui AGENTS.md §9 受管面段。
25+
- ⇒ 命中即整 PR 停在 draft 等人合,⛔ 不 ready、不入队、不 auto-merge、不自合、不留批准。
26+
- 未命中的 PR 按同节走合并队列落地;changeset 与版本纪律见 objectui AGENTS.md §9。
27+
- 半状态巡查在本仓有载体:定时 workflow 与 sweeper 皆在,自 objectstack 移植。
28+
- 锚 issue 未配置 ⇒ 写锚步骤 skip,发现只落 run summary,绿不等于已交付读锚腿。
29+
- ⇒ 手工半边照 `hotcrm.md` 那条办,并每轮实测锚是否已配,⛔ 不沿用本行读数。
30+
31+
## 席内判断
32+
33+
- 卡正文是快照不是状态:⛔ 不凭正文单读裁决或派发,读全线程并对 `origin/main` 重测。
34+
- `pm:queue` 在本仓兼指已分诊与可派发 ⇒ 逐张重验可派性,⛔ 不按标签直派。
35+
- 零读数恒配点亮的正控,且正控须经被测机制本身生效;本仓 search 假零,查重只引命中。
36+
- 脚本能滚动不证明用户能滚动:可达性断言必须驱动真实输入管道。
37+
- 落地验证按内容,正控须在合并前证明会响,⛔ 不按 sha 判已落地。
38+
- 档位与复核豁免边界读源码与 `references/contract-review.md`,⛔ 不凭记忆或继承的注记。

0 commit comments

Comments
 (0)