Skip to content

Commit 30bc3e7

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-12795-verify-lock-throughput-measurement
2 parents 244b729 + 09b0d7b commit 30bc3e7

19 files changed

Lines changed: 1602 additions & 93 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
Security fix (fail-closed tightening, #13552): the RLS emptied-membership deny guard is now polarity-aware. A policy whose pre-resolved membership set resolves EMPTY under a negated membership test (`not in` — e.g. `using: '!(owner in current_user.org_user_ids)'`) now compiles to the deny sentinel (zero rows) instead of flowing through. Before this fix `$in: []` under `$not` inverted to a constant-TRUE clause (`NOT (1 = 0)` on the SQL read-scope lowering), so the policy the guard exists to turn into a DENY compiled to ALLOW-ALL on reads. The guard now fires at any composition depth: `$not` wrapping the membership directly, `$not` arms nested inside `$or`/`$and`, `$not` over a composite containing the membership, and multi-level `$not` (odd polarity anywhere; the bare positive case is unchanged).
6+
7+
Blast radius, in plain terms: callers that were relying on that allow-all stop seeing rows. If a negated-membership policy was the only applicable policy and its membership set resolves empty (no active organization; an empty team/territory/blocked set), reads that previously returned EVERY row now return ZERO rows. The prior behaviour was a defect — an over-permissive read on a row-level-security scope — not a contract. If own-rows access must survive an emptied membership set, author it as a separate OR'd policy (e.g. `owner == current_user.id`): each policy's grant is compiled independently, and a sibling policy dropping does not take it down. A deliberate allow-all remains authorable as a literal `true` predicate. Unchanged: a NON-empty membership set under `not in` compiles and enforces exactly as before, and an emptied POSITIVE membership nested in `$or` (e.g. `owner in current_user.team_ids || owner == current_user.id`) still preserves the other arm's grant.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/service-storage": patch
3+
---
4+
5+
fix(service-storage): stamp the acting organization on the last two `sys_file` insert doors (#13547)
6+
7+
`sys_file` declares no `tenancy` key, so `isTenancyDisabled()` reads `false`
8+
and the registry provisions `organization_id` on it. Four doors on the object
9+
had been given the acting organization one card at a time — `createFile`
10+
(#12745), `createSession` (#12928), and the `update`/`delete` halves (#13178) —
11+
and all four run through `StorageMetadataStore`, which threads a
12+
`StorageWriteContext` into `context.tenantId` so the platform's insert-side
13+
chokepoint can stamp the column.
14+
15+
Two doors bypassed that store entirely and carried no organization at all:
16+
17+
- `copyOwnedFile` (`file-reference-lifecycle.ts`) — the copy-on-claim
18+
lifecycle hook, which inserts a fresh `sys_file` whenever a record writes an
19+
id already owned by another field slot;
20+
- `materializeDataUri` (`backfill-file-references.ts`) — the operator backfill
21+
pass, which inserts one `sys_file` per inline `data:` URI it converts.
22+
23+
Both passed `{ isSystem: true, [RAW_FILE_VALUES_CONTEXT_KEY]: true }`, so
24+
`buildDriverOptions` emitted no `DriverOptions.tenantId`,
25+
`SqlDriver.injectTenantOnInsert` had nothing to stamp from, and every row
26+
landed `organization_id = NULL`. The driver's tenant term is
27+
`(organization_id = :tenantId OR organization_id IS NULL)`, so those rows were
28+
reachable from **every** organization — including through the very update and
29+
delete doors #13178 had just scoped.
30+
31+
⚠️ Nothing warned, and the silence was explained rather than reassuring:
32+
`isSystem` also sets `bypassTenantAudit = true`, which is exactly the guard
33+
`auditMissingTenant` returns at — so the `[tenant-audit]` line naming this
34+
defect ("writes will not be tenant-isolated") never fired for either door.
35+
36+
Each door now threads the organization the platform can actually justify, as
37+
an execution context — ⛔ never as a column on the payload, so
38+
`resolveTenantField` / `injectTenantOnInsert` keep deciding whether the object
39+
has a tenant column and whether an explicit value wins:
40+
41+
- the **copy** takes the organization of the write that triggered it, read
42+
from `HookContext.session.organizationId` (which ObjectQL's `buildSession()`
43+
copies verbatim from `ExecutionContext.tenantId`);
44+
- the **backfill** takes the organization of the record whose field held the
45+
bytes, resolved with the same `createWallOrganizationResolver` the `sys_file`
46+
organization sweep uses, so an object declaring `tenancy.tenantField` is read
47+
by the column it is really walled by.
48+
49+
Both stamp exactly what that sweep would independently derive from the new
50+
file's field-reference holder, so the forward and repair halves agree by
51+
construction. The backfill needs **no** operator-supplied organization and
52+
deliberately takes none: one run spans every object and organization in the
53+
deployment, so a single supplied value would be stamped onto other tenants'
54+
files — and a wrongly-stamped row is walled into somebody else's tenant, which
55+
is strictly worse than a NULL row that stays reachable.
56+
57+
Where no organization is in scope — a caller with no active org, an unwalled
58+
object, a legacy row that carries none — the `tenantId` key is omitted
59+
entirely and the write proceeds exactly as before. ⛔ Forward-stamping only:
60+
no existing `sys_file` row's organization is written by either door.

.claude/agents/os-dev.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,9 @@ not decoration:**
412412
2026-08-04:这是创业项目,核心能力优先):能力扩张默认从紧,无拉动的声明面按
413413
implementation-first 处置,已发布零消费的能力不因沉没成本获得豁免。
414414

415-
Your recommendation must be justified on all four axes;四轴冲突时如实呈现权衡,交维护
416-
者拍板。同样,`main` 在你脚下碎了、依赖未合并、CI 基础设施故障时,返回 `blocked`(附证
417-
据)—— 先重试到足以确认不是你的改动
415+
Your recommendation must be justified on all four axes;四轴冲突时推荐按拉动排序:实测拉动→长远,
416+
零拉动→不扩散,防错破平,安全与难逆恒人工(只排推荐);权衡如实呈现交维护者拍板。`main`
417+
在你脚下碎了、依赖未合并、CI 基础设施故障⇒`blocked`(附证据),重试到足以排除你的改动
418418

419419
## 终报消息 —— 恰好这段 JSON,不带任何环绕散文
420420

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,9 +868,17 @@ finding 定级换结果态,永不留挂)/ `Blocked-by:` 活性现验(合并一
868868
- **创业阶段不扩散需求****创业阶段聚焦原则**(维护者 2026-08-04 指示:
869869
「我们是一个创业项目,应该先专注于核心能力」):能力扩张默认从紧,
870870
无拉动的声明面按 implementation-first 处置,已发布零消费的能力不因沉没成本获得豁免。
871+
**过渡也从紧 —— 创业阶段不渐进**(维护者 2026-08-27 裁,逐字:「项目在创业阶段,
872+
用户也很少,短期不考虑渐进。」):废弃别名/拼写与能力退役默认**立即退休**,
873+
不设分阶段窗口、不留双拼写宽限;staged 选项仅凭具名外部用户证据才可呈报为推荐。
871874

872875
推荐意见必须基于这四条轴给出理由;四轴冲突时如实呈现权衡,
873-
交维护者拍板。**标准四棱卡面块是落卡与升级的必备件**(四棱维护者 2026-08-11 接受;
876+
交维护者拍板。**分歧推荐序按拉动定向**(维护者 2026-08-27 裁「tong y 4」):
877+
有实测拉动 ⇒ 荐长远形态一次付清;零拉动 ⇒ 荐不扩散(defer/remove、记录在案、重开自由);
878+
防错轴破余下平局向响亮/结构性;安全/权限边界与破坏性难逆动作恒在人工地板。
879+
⛔ 此序只排推荐:分歧块照旧升级,四棱同向置信门与代裁面不变,
880+
细则见 `references/decision-analysis.md`
881+
**标准四棱卡面块是落卡与升级的必备件**(四棱维护者 2026-08-11 接受;
874882
标准块 2026-08-18 裁定「同意」;同裁定的摘要视图/批量决裁通道已退役,
875883
墓碑与原话见 `references/lanes/director.md`;收件箱由维护者定期与 AI 讨论消化,⛔ 不 assign
876884
推送 —— 维护者 2026-08-19 裁定:「我感觉决策卡推给我太麻烦了,我需要和ai讨论才能判断,
@@ -904,6 +912,11 @@ skills和决裁。」的归属半句,skills 面与其车道照旧归 skills 席)
904912
发布动作的完整清单与「围绕发布的工作照旧归座位」细则以 AGENTS.md「release act」
905913
段为权威,含**合并 Version Packages PR**)。发现未经人工的发布痕迹按事故立案,
906914
⛔ 不代跑补救性发布 —— 机械通道的存在不构成授权,遇到那类通道当缺陷上报。
915+
- **发版提醒纪律**(维护者 2026-08-27 拍板,逐字:「要发版本我会自己点。
916+
除非重要修复要发版本你可以提醒我。」):Version Packages PR 默认永不催 ——
917+
挂着的发布 PR 是常态,⛔ 不进维护者摘要;唯一例外 = 重要修复已合 main 在等发布,
918+
且提醒必须点名该修复与不发版的用户可见后果,让维护者仅凭提醒即可判缓急;
919+
与上条相组合 —— 本条管**何时开口**,上条管**谁动手**
907920
- **Governed 面由维护者人工合并,合并即审核记录**(三裁一脉:2026-08-08
908921
「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并」;2026-08-11「所有 skills 的更新和adr
909922
类似,需要人工审核」;2026-08-18 对「人工合并即人工审核,事后审计代替事前门」

.claude/skills/pm-dispatch/references/decision-analysis.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@
4242
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
4343
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**
4444
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
45+
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
46+
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ issue 编号;机器判据恒英文,引文逐字不译。
2525
策」四轴(`check:skill-frame-sync` 管的四份拷贝不动,⛔ 本文不复制框架本体)。
2626
- 本原则重锚 2026-08-26 流程裁定(「决裁流程今天这样很好,决策卡5张一组,从业务的角度给我具体解
2727
释,说明可选项,并进行四维分析。应该写入技能。」):流程未变,2026-08-26 定流程、2026-08-27 升格定归属。
28+
- **创业阶段不渐进**(维护者 2026-08-27,逐字:「创业阶段不渐进 应该写入项目总监skills」):
29+
废弃别名与退役默认立即退休,无窗口无双拼写宽限;staged 仅凭具名外部用户证据可荐。
2830

2931
## 职责一:`needs:contract-review` 复审链
3032

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
- **PR 形态**:存在、是 draft、目标 `main`、正文首行引用卡片
77
—— **`Fixes #<n>` 仅当合并应当关卡**;只落地了可实施一半(另一半在决策箱或按范围排除)
8-
⇒ 必须 `Part of #<n>`,否则合并会静默关掉一张正躺在决策箱里的卡,
9-
`needs-user-decision` 的收件箱过滤只看 open issue。翻 ready 之前亲核首行,别只信报告。
10-
- **闭合关键词两读**:
11-
翻 ready 前亲扫正文 —— 合并前唯一的人工关口(⛔ 关键词永不挨着另一张 open 卡编号;
12-
解析器不理会否定句);合并后那一读见落地细则,解析事实见平台读数。
8+
⇒ 必须 `Part of #<n>`,否则合并静默关掉决策箱里的卡(收件箱过滤只看 open)。⚠️ 切
9+
`Part of` 的正文最易再武装自动关闭:解释「为何不关」的记账句(谁在合并后手动关卡)、
10+
否定句、引号/反引号包裹,解析器一律照关;安全拼法 = 卡号旁零动词,关闭安排写进评论。
11+
- **闭合关键词两读**:翻 ready 前亲核首行、亲扫全正文(⛔ 关键词永不挨另一张 open 卡编号)
12+
—— 合并前唯一的人工关口,别只信报告;合并后那一读见落地细则,解析事实见平台读数。
1313
- **`Part of` 收口的卡不会自动关,`pm:dispatched` 必须手工摘**(动作要件见落地细则 B):
1414
ACCEPT 那一刻记进落地待办,⛔ 不留给下次巡检 —— 漏摘让在飞视图多算一张无主开卡。
1515
- **范围检查**(取 changed files,⛔ 不看报告自述):无 `content/docs/releases/` 改动、

.github/workflows/half-state-patrol.yml

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,36 @@ name: Half-State Patrol
1313
# counterfactual, and an alarm added to a script nobody runs is still silence.
1414
#
1515
# "Some seat should run it" also kept not happening for a MEASURED reason, not a
16-
# discipline one: the live sweep cannot run inside a PM session container at all
17-
# (#7412 class 1 — api.github.com refuses that egress in both directions, with and
18-
# without a token). The fix therefore had to move the caller somewhere the
19-
# transport prerequisite is actually met. A GitHub Actions runner with the
20-
# workflow's own `GITHUB_TOKEN` is that place — #7412 class 2, the triage Routine
21-
# container, is the same shape and measured reachable with 15,000 core quota.
16+
# discipline one: in every container class measured at the time, the live sweep
17+
# could not run at all (#7412 class 1 — api.github.com refuses that egress in
18+
# both directions, with and without a token). The fix therefore had to move the
19+
# caller somewhere the transport prerequisite is actually met. A GitHub Actions
20+
# runner with the workflow's own `GITHUB_TOKEN` is that place — #7412 class 2,
21+
# the triage Routine container, is the same shape and measured reachable with
22+
# 15,000 core quota.
23+
#
24+
# ⚠️ CORRECTED (#13544, measured 2026-08-31): "cannot run inside an agent
25+
# container at all" is no longer true of every such container, and the claim
26+
# above is kept only as the history that put this workflow here. A proxied agent
27+
# container reaches api.github.com fully — `/rate_limit`, `/user` AND
28+
# `GET /repos/{owner}/{repo}` all 200 with `server: github.com` — once node's
29+
# fetch is routed through the session proxy, which the script now does for
30+
# itself. What had actually failed was the ROUTE: node's `fetch` ignores
31+
# `HTTPS_PROXY`, so the sweeper sent the proxy's placeholder token straight to
32+
# GitHub, earned a 401, and reported that refusal as the container's verdict.
33+
#
34+
# ⛔ That does NOT retire this workflow, and the fix deliberately did not touch
35+
# it. The #9844 reason stands on its own and is not a transport reason: an alarm
36+
# whose only caller is "a seat should remember" is silence, whoever CAN run it.
37+
# The on-demand path is restored BESIDE the schedule — a lane that needs the
38+
# board read right now (the 4x/day body trims its own rows, and says so) can now
39+
# get it — never instead of the schedule.
40+
#
41+
# What this means for a runner is nothing at all, and that is load-bearing given
42+
# this file is copied VERBATIM into sibling repos (#11217): the script re-execs
43+
# only when `HTTPS_PROXY`/`https_proxy` is set, and a runner sets neither. No
44+
# child process, no extra line, no behaviour change — pinned in the script's
45+
# `--self-test` and measured byte-identical on the probe output.
2246
#
2347
# ## What lands where
2448
#

0 commit comments

Comments
 (0)