Repository navigation
[Decision] group posture is an on-premise shape — should package-authored scheduled flows run under group with the switch on, and which organization do their writes carry? (ruling G item 3, reopened by the maintainer) #18378
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 16, 2026 Ruling — A′ (maintainer, 2026-09-16)
Maintainer, verbatim and untranslated (live PM chat), ruling on the options as put to them:
同意并处理
The option agreed to is A′ — the card's A with its record-less fallback replaced by a refusal. A′ is this seat's wording, not the maintainer's; it was put to them as the recommended arm beside the card's own A / B / D and selected. Stated in full:
group+ switch on binds without a declaration, likesingle.time_relative: the run carries the swept record'sorganization_id; writes follow the record.- cron
schedule(no record): carriesconfig.organizationwhen declared; when it is not declared, the write is refused loudly (ADR-0112 envelope) — ⛔ never a guessed fallback organization. - Group-wide reads stay group-wide (inherent to the posture).
- The boot/audit line names the effective binding in both directions.
⇒ Card options A, B, C and D are closed. A′ = A's binding + A's record-derived writes + D's refusal on the record-less cron cell only.
Why A′ and not the card's A — two findings that moved it
Both were measured on
origin/mainbefore the ruling and are the whole of the delta:-
A's
slug='default'fallback does not reliably exist undergroup, and where it does it is probably not the head office. Under a wallAuthPluginskips its own default-organization bootstrap and hands the job to the enterprise@objectstack/organizationsruntime, whose helper is admin-keyed: it binds the account resolved by the config anchor or the legacy cross-tenant grant, and answersno_adminfor anyone else — so agroupinstall with no resolvable platform admin bootstraps no organization at all (packages/plugins/organizations/src/walled-default-org-self-registrant.pin.test.ts). Where one does exist it is whichever organization the platform owner registered under — on a multi-plant install, plausibly a plant rather than the group's head office. A's fallback therefore lands a group-wide cron's notifications in one arbitrary plant's inbox, silently. That is sharper than the card's ③ axis recorded it ("可能没人读"): the failure is a wrong owner, not an unread one, and a wrongorganization_idis authoritative to every report, export and cleanup script that filters by organization. A′ avoids it, and avoids amending ADR-0093 D3 (defaultOrgId()answersnullunder any wall) or standing up a second resolver beside it. -
A's record-derived half is already the resolution order elsewhere, so it is a narrowing of drift rather than a new principle.
suspended-run-store.ts:916resolves ownership asorganizationOf(<record>) ?? ctx.tenantId ?? null— subject first, context second — and the run-history row is stamped the same way (recorded inpackages/qa/dogfood/test/schedule-sweep-organization-scope.dogfood.test.ts's header). This closes the card's confidence gap ② on the record-derived side: the other consumers ofsys_automation_run/ inbox do not assume a declaredtenantIdundergroup. The gap survives only for the record-less cron cell, which is exactly the cell A′ refuses.
Governing text
- ADR-0105 D1 /
packages/spec/src/security/tenancy-posture.ts—group= organizations as membership boundaries over one shared dataset, with "group-wide visibility and cross-org workflow … inherent to the shape";postureEnforcesWall('group'),postureStampsOrganization('group'),postureUsesUnionScope('group')all true. - ADR-0093 D3 — target-org resolution consumes declared mode;
defaultOrgId()answersnullunder any wall. packages/objectql/src/tenancy/system-write-organization.ts:263—resolveSystemWriteOrganizationrefuseswalled-posturewithout probing the database, so the refusal is unconditional ongrouptoday. Re-check:git grep -n "walled-posture" origin/main -- packages/objectql/src/tenancy/system-write-organization.ts(positive control in the same file:ambiguous-organization).packages/spec/src/automation/schedule-organization.zod.ts— carries the 2026-09-08 A schedule-triggered flow'snotifydelivers nothing on a multi-organization install: the run carries no organization, so the tenant-scoped inbox/delivery writes are refused (#8844) while the run reads healthy #16659 ruling verbatim. This is the protocol prose A′ changes, and Prime Directive Add comprehensive test suite for Zod schema validation #12 (contract-first) puts that change inpackages/specfirst.- AGENTS.md Prime Directive chore: version packages #10 — the boot/audit line owes the operator the effective state; a refusal owes the remedy by name.
Scope of the reopening, unchanged
⛔ Nothing about
singleorisolatedis reopened. Ruling G item 3 stands forisolated; the deployment switch itself (OS_AUTOMATION_SCHEDULED_WORK_ENABLED, default OFF in every posture) is untouched — A′ decides only what binds once the operator has turned it on undergroup.Blocked-by — cleared, verified live
The card's 裁后执行 gated execution on PR #18198 ("在 PR #18198 落地之后再动, ⛔ 不并入它"). Verified at ruling time, not recalled: #18198 is merged (
merged_at2026-09-16T09:01:54Z,merged: true). The gate is discharged and nothing was merged into that PR. This card moves straight to a working state rather thanpm:blocked.
Generated by Claude Code
Claim: session_01URii26ZSYx4xPZ9ai47ceH · branch
claude/zealous-mendel-o0o6aqExecuting ruling A′ (
Ruling-ref: 5695424700, retrieved this pass).Blocked-by:#18198 discharged — verified live, not recalled:merged: true, andresolveScheduledWorkPolicynow resolves onorigin/main(git grep -c "resolveScheduledWorkPolicy" origin/main -- packages/types/src/env.ts→ 1).Execution order follows the card's 裁后执行 with A's fallback cell replaced per the ruling:
packages/specfirst (Prime Directive #12 — 协议不正确的应该先修改协议), then types / triggers / service-automation, then docs + changeset.⛔ The branch name is the session's designated branch rather than
claude/issue-18378-*; this comment is the discoverability record for #18378.
Generated by Claude Code
Landed — PR #18420 merged 2026-09-18T15:35Z
Ruling A′ (
Ruling-ref: 5695424700) is implemented and onmain.Fixes #18378auto-closed this card;pm:dispatchedand the assignee were cleared by hand in the same action.What landed, per posture, with the switch on:
posture declaration a bound run's writes act as singlenot read nothing — the install's one organization resolves beneath each write groupoptional declared ⇒ the declaration; undeclared ⇒ the swept record's own organization isolatedrequired unchanged — undeclared ⇒ not armed A record-less run under
groupthat declared nothing resolves nothing and takes the existingwalled-posturerefusal at its first tenant-scoped write, by name. ⛔ The card's option-A fallback to the bootstrap organization (slug='default') was not implemented: it was measured as unreliable under a wall (admin-keyed bootstrap, may not exist) and, where it exists, plausibly one plant rather than the group's head office — a wrong owner, silently authoritative to every report that filters by organization.One design change the card did not anticipate. The first implementation read the swept record's organization through the STAMP resolver, which made the sweep a fourth consumer of
tenancy.organizationField— a key whose contract pins its consumers to three named platform-row writers. That was caught by the independent contract review and answered by redesign rather than by a ruling:@objectstack/metadata-corenow has a WALL face (resolveRecordWallOrganizationField/createRecordWallOrganizationResolver) that shares limbs 1–4 with the stamp face and skips limb 0 entirely, and the sweep binds that. The key's consumer list is untouched.Follow-ups filed from this work:
- Retire
tenancy.organizationFieldfrom the authorable surface — one platform table's fact, not customer configuration #19054 — retiretenancy.organizationFieldfrom the authorable surface (maintainer ruling 2026-09-18: 「organizationField 撤出可授权面 同意你的建议」). Measured: exactly one shipped object declares it. - Retire the in-seat clause-② contract review — the rule, its gate label, and the 9k-line checker's gate role #19061 — retire the in-seat clause-② contract review, its gate label and the checker's gate role (maintainer ruling 2026-09-18: 「不需要 AI 不能自审契约改动 这种」).
Contract review disposition: an isolated review subagent returned PASS WITH FINDINGS; all three findings (a changeset missing
@objectstack/cli, a stale docblock still claiming the retired "any walled posture" rule, a grammar defect in a published.describe()) were verified against the tree and fixed ind563fac6.⚠️ The machine-read pair on that verdict is UNJUDGED rather than clean — recorded on the PR, and the rule behind it is what #19061 retires.
Generated by Claude Code
- Retire
- added a commit that references this issue
on Sep 28, 2026
Filed by the director seat (
session_01WCEaPsmKY4UyoivKkkaUHt) on the maintainer's instruction during the 2026-09-16 design discussion of PR #18198. Maintainer, verbatim and untranslated (live PM chat):⇒ Ruling G item 3 (#17396, 5642381255: 「
groupis off by default, likeisolated」, and with the switch on 「behaves as walled」) is reopened forgrouponly by the maintainer's own words; it stays a tenancy-boundary decision, so this seat files the card and does not rule it. ⛔ Nothing aboutsingleorisolatedis reopened; ⛔ nothing here delays PR #18198.一句话问题
一个集团版本地部署(
group姿态:多个工厂或分公司作为组织,共用一个数据库)把定时开关打开之后,包内的定时流要不要跑;跑的时候,它写出来的通知、运行记录、待办归哪个组织。Governing text
resolveSystemWriteOrganizationrefuses an organization-less system insert under any wall (walled-posture), andTenancyService.defaultOrgId()answersnullunder any wall by ADR-0093 D3. An organization-less scheduled run ingroupcould read the whole group and update records in place, but the inbox, delivery andsys_automation_runrows it inserts would be refused … Which organization a group-wide sweep's inserts belong to is the part the maintainer said is not yet thought through」.group= 「organizations share one database as data」 with membership/invitation boundaries, group-wide visibility and cross-org workflow inherent to the shape (the multi-plant MES example is the ADR's own);postureEnforcesWall('group') = true,postureStampsOrganization('group') = true(packages/spec/src/security/tenancy-posture.ts).single⇒ the bootstrap organization (slug='default'); a walled mode ⇒ none, the framework never guesses.notifydelivers nothing on a multi-organization install: the run carries no organization, so the tenant-scoped inbox/delivery writes are refused (#8844) while the run reads healthy #16659 (「多组织定时任务本来只能在组织内运行,应该带组织ID,不允许跨组织的定时任务」) — made for the multi-tenant shape; whether it binds a group (one legal group, cross-org visibility inherent) is exactly what this card asks.resolveScheduledWorkPolicy()(packages/types/src/env.ts) answersgroup+ on ⇒requiresActingOrganization: true;schedule-organization.zod.ts's docblock and.describe()say the declaration is required under a walled posture. Option A changes that protocol prose and that row — 「协议不正确的应该先修改协议」 applies, so the change is inpackages/spec/packages/typesfirst.Premises, with re-check commands
Options × real cost
group+ on binds without a declaration, likesingle. Writes follow the record: atime_relativerun carries the swept record'sorganization_id; a cronschedulerun that has no record carriesconfig.organizationif declared, otherwise the group's bootstrap organization (slug='default'), and the boot/audit line states which. Group-wide reads stay group-wide (inherent to the posture).groupstays walled; declaration required; packaged flows do not run; admins clone per organization (ADR-0126 §7.1)group: one run per organization, reads scoped to it业务含义直译
A = 集团总部的定时任务照常跑,单据写到它所属的工厂,没有单据的写到总部。B = 每个工厂各自复制一份任务。C = 每个工厂各跑一份,总部看不到跨厂的汇总。D = 跑可以,写不清归属就当场拒绝。
四轴(从业务立场)
group按 ADR-0105 就是「一家集团、多个分支、一个数据库」,集团级批处理是这个形状的固有能力(主流 ERP 多工厂、Salesforce/Dynamics 多业务部门都在一个 org 内跑全局批处理,行级归属由记录自己决定)。A 只新增一条「无记录时归总部」的兜底规则;C 新增一套扇出机制;B 把成本永久推给管理员。⇒ A。group本地部署要跑包内定时流只能逐组织克隆;维护者本人指出group是本地部署、应可运行。group部署在线。os doctor打印。D 最响亮但把失败推到运行时。B/C 无此风险但代价见①。Prior rulings read: group,posture,schedule,organization,wall,defaultorgid,time_relative,bootstrap → 66 hits; ADR-0105 D1/D4/D5, ADR-0131 D3, ADR-0057 D9, ADR-0099 D2, ADR-0106 D6, ADR-0120 D5 — none rules the write-ownership of an organization-less scheduled run under
group; ruling G item 3 recorded it as unanswered.推荐:A(回退 D)。自检:只看①选 A;②③④ 是否翻转:否(③ 只要求兜底被响亮说出)。
置信缺口:① 09-08 「不允许跨组织的定时任务」是否有意覆盖
group(其上下文是多租户),只有维护者能答;②sys_automation_run/ inbox 的其他消费者是否假定group下的tenantId必来自声明;③ 现网group部署数未测。裁后执行
packages/spec先:schedule-organization.zod.ts的 docblock 与.describe()、ADR-0087 条目 18 的group行),再 types/triggers/service-automation:resolveScheduledWorkPolicy的group行改为不要求声明并带写归属规则;time_relative在group下把tenantId从被扫记录填入(退掉「never filled from the swept row」在 group 上的那半钉子);无记录的 cron 走config.organization否则 bootstrap 组织,boot/audit 行点名;文档四页;changesetminor+ BREAKING;Clause-②: yes。在 PR feat(types,triggers,service-automation,runtime,cli,spec,lint)!: package-authored scheduled work is a deployment decision, off by default #18198 落地之后再动,⛔ 不并入它。Refs: #17396 (ruling G) · PR #18198 · #16659 (2026-09-08 ruling) · ADR-0105 · ADR-0093 D3 · ADR-0126 §7.1
os-decision-facets
group是一家集团一个库,集团级批处理是固有能力;A 一条兜底规则,C 一套机制,B 永久人工。group本地部署今天只能逐组织克隆;维护者指出应可运行;部署数未测。维护者速读
事情。 您说
group(集团版本地部署,多个工厂或分公司共用一个库)应该可以跑定时任务。裁决 G 当时把group和云上多租户一样默认关、开了也要每条流声明组织,原因是那时没想清楚:一条集团级定时任务写出来的通知和运行记录归哪个组织——在围墙姿态下,没有组织的系统写入会被拒绝。选项。 A 像单租户一样直接跑;写入跟着记录走(扫描到哪个工厂的单据就归哪个工厂),没有单据的任务归总部组织,启动日志写明。B 维持现状,每个工厂自己克隆一份。C 自动给每个组织各跑一份,互不可见。D 跑可以,但写不清归属的当场拒绝。
席位意见。 推荐 A:
group的定义就是一家集团一个库,跨分支可见是它的本意;A 只多一条兜底规则,并且在日志里说清楚。回退 D。你要做的:回一个字母 —— A / B / C / D。
Generated by Claude Code