Repository navigation
Description
发现于 #6155 的前提核验(未在该 PR 内修,按 Prime Directive #10 单开)。
事实(实测,非推断)
flow 在 DEFAULT_METADATA_TYPE_REGISTRY 里是 allowOrgOverride: true + allowRuntimeCreate: true,所以 runtime/src/domains/meta.ts:158-159 的 saveMetaItem({ type, name, item, organizationId })(org 取 resolveActiveOrganizationId)会写出 sys_metadata.organization_id = 'org_a' 的 flow 行。实测确认该行确实落库。
之后这一行的命运是不对称的:
| 时机 | 路径 | org 行可见? |
|---|---|---|
| 发布当场(同进程) | saveMetaItem → applyRegistryWriteThrough(publish 分支)→ hydrateOverlayIntoRegistry → 进程级 SchemaRegistry |
✅ 可见 |
| 发布当场,自动化重绑 | emitMetadataMutation → metadata:reloaded → resyncFlowsFromProtocol → getMetaItems({ type: 'flow' })(读到 registry 分支) |
✅ 绑定,定时任务起飞 |
| 重启后冷启动 | ObjectQLPlugin.restoreMetadataFromDb → protocol.loadMetaFromDb(),where = { state:'active', organization_id: null }(metadata-protocol/src/protocol.ts:10620-10623) |
❌ 跳过 |
| 重启后 kernel:ready 绑定 | service-automation/src/plugin.ts:1542 protocol.getMetaItems({ type: 'flow' }) —— 不传 organizationId,于是 protocol.ts:3319 的 orgRecords = orgId ? … : [] 为空 |
❌ 跳过 |
实测(真 ObjectStackProtocolImplementation + 内存 sys_metadata 引擎,两行 flow:org_sweep org=org_a、platform_sweep org=null):
PROBE rows = [{"name":"org_sweep","org":"org_a"},{"name":"platform_sweep","org":null}]
PROBE getMetaItems({type:flow}) names(registry 为空时) = ["platform_sweep"]
PROBE getMetaItems({type:flow}) names(publish 写穿后) = ["org_sweep","platform_sweep"]
PROBE loadMetaFromDb = {"loaded":1,...} // 只有 platform_sweep
影响
- 静默失效:租户在 Studio 里发布一个 org 归属的 flow(record-change 或 schedule),当天正常触发;下一次进程重启后它再也不触发,没有任何日志说它消失了——
kernel:bootstrapped的 unbound 审计也看不见它(它压根没注册)。这正是 AGENTS.md「Absence must be loud」的反面。 - 可见性不对称:写穿期间该 org 行进了进程级 registry,于是
getMetaItems({ type: 'flow' })对任何 org 的调用方都返回它——而loadMetaFromDb的注释明确说 per-org overlay 之所以不 hydrate 就是「to avoid cross-org leakage into the process-wide SchemaRegistry」。写路径比读路径更宽松,恰好是applyRegistryWriteThroughTSDoc 自己声明要避免的("The write must not be more permissive about that than the read is" —— 该 gate 只挡了environmentId !== undefined,没挡 org)。
与 #6155 的关系
不是 #6155 的子问题:#6155 问的是「定时流程的 org 从哪来」,本单问的是「org 归属的 flow 到底还活不活」。但两者相互制约——若维护者裁决 org flow 本就不应被加载(ADR-0005 的白名单表原文即「automation flow ❌ per-org override,Per-org variants are a deployment, not an overlay」,与代码现状矛盾,另见同批开的 registry/ADR 漂移单),那本单的修法就是在写入侧拒绝而不是在读取侧补齐。所以建议先定性再定修法。
复现
无需真 DB:packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts 的 stub 引擎即可(需给 registry 补 isPackageDisabled: () = false 与一个真存的 listItems)。
Refs:#6155、#4636(loadMetaFromDb object 分支的另一处 row 读取缺陷)、ADR-0005 §Tenant-customizable type whitelist、applyRegistryWriteThrough(protocol.ts:7434)。
Blocked-by: #6191
Generated by Claude Code
Activity
claude commentedon Aug 7, 2026
Triage:
pm:blocked(Blocked-by: #6191) +domain:engine-core+target:v17.Classification — real, well-located, but its fix direction is gated
Every fact in the body reproduces against
origin/main:flowcarriesallowOrgOverride: true(packages/spec/src/kernel/metadata-plugin.zod.ts:649),loadMetaFromDbfiltersorganization_id: null(packages/metadata-protocol/src/protocol.ts:10622), and the cold-boot binder callsprotocol.getMetaItems({ type: 'flow' })with noorganizationId(packages/services/service-automation/src/plugin.ts:1542). The write/read asymmetry is exactly as documented.Not queued, because the body's own closing line is right: the two candidate fixes are opposite. If ADR-0005's ❌ row stands, the fix is to refuse the write; if the reversal is ratified, the fix is to complete the read. A dev dispatched today would hit the os-dev "underspecified decision that changes the public contract → STOP" clause and burn a run. #6191 is now in the maintainer's decision inbox and is the gate, so this issue takes
pm:blockedwith a machine-readableBlocked-by:line added to the body — the unlock sweep returns it to the queue when #6191 closes. This is a deferral of direction, not of priority.Routing rationale (landing site, not title vocabulary)
domain:engine-core. The issue reads as automation, and the most visible symptom is aservice-automationone — but three of the four named code sites are inpackages/metadata-protocol(domain:engine-coreper the domain table'spackages/metadata*family): the read filter atprotocol.ts:10620-10623, theorgRecords = orgId ? … : []branch at:3319, and the write-through gateapplyRegistryWriteThroughat:7434whose own TSDoc states the write must not be more permissive than the read. Theservice-automationcall site atplugin.ts:1542is a consumer of the flattened view, and it is correct under the current contract — it cannot pass anorganizationIdit has no business knowing atkernel:ready. Under either ruling the asymmetry is resolved in metadata-protocol (or, for the refuse-the-write route, in thepackages/specflag that #6191 owns), not in the binder. Labeling thisdomain:servicesoff the title would route it away from the package the fix lands in.One part of this is a defect under either ruling
Worth pricing separately when the gate lifts: impact ② (the publish-time write-through hydrates an
org_arow into the process-wide SchemaRegistry, sogetMetaItems({ type: 'flow' })returns it to callers of any org) is a cross-org visibility leak regardless of which direction #6191 goes —loadMetaFromDb's comment says avoiding exactly that leak is why per-org overlays are not hydrated. If the maintainer's ruling is slow, that half is separable and dispatchable on its own.Dedup
Full-text scan of all open issues and PRs across the three repos (
loadMetaFromDb,OVERLAY_ALLOWED_TYPES,resyncFlowsFromProtocol,applyRegistryWriteThrough,allowOrgOverride): no existing dispatch entry. Neighbours checked and distinct:record.packageIdfrom a snake_case row — always undefined, every object overlay registers under the 'sys_metadata' sentinel at boot #4636 (pm:dispatched,domain:engine-core) — alsoloadMetaFromDb, but therecord.packageIdsnake_case read on the object branch. Different line, different failure; same function, so whoever takes this one should expect a same-file serial constraint against loadMetaFromDb object branch readsrecord.packageIdfrom a snake_case row — always undefined, every object overlay registers under the 'sys_metadata' sentinel at boot #4636's PR.needs-user-decision,domain:services,target:v17) — where a scheduled flow's org comes from, not whether an org-scoped flow survives a restart. Correctly kept separate by the author.flow/workflow/approval一律 ❌ per-org override,注册表却写着allowOrgOverride: true——反转没有走 ADR(Prime Directive #13) #6191 — the gate; cross-linked both ways.Cross-repo: nothing in objectui/cloud.
Release board
target:v17on criterion ① and ④: a tenant publishes an org-scoped flow in Studio, it fires all day, and after the next process restart it never fires again with no log line anywhere — silent loss on a shipped path, and the kind of thing release notes would have to apologise for. Note the label is applied on the symptom, independent of which fix direction #6191 picks; if the ruling is "org flows should never have been writable", the board item becomes the write-side rejection plus whatever migration the already-stored rows need, which is still v17 work.Body edit
Appended one line to the body —
Blocked-by: objectstack-ai/objectstack#6191— per the label discipline thatpm:blockedcarries its machine half in the body. No other text touched.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
hotlong commentedon Aug 7, 2026
迁移:
domain:engine-core→domain:metadata(维护者 2026-08-07 批准拆分;座位贴 #6367,SKILL PR #6370)。落点证据:冷启动两条读路径滤掉organization_id非空行 ——loadMetaFromDb(protocol.ts:10620)+getMetaItemsorg 分支,metadata 冷启动加载/水合。pm:blocked状态与正文Blocked-by:行随卡走。全量分类审计 49 卡三判,本卡 MOVE/high。分诊会话:session_01BickTBKm2JYSNnrtPT8ysa。Generated by Claude Code
baozhoutao commentedon Aug 8, 2026
Unblocked —
pm:blocked→pm:queue(domain:metadata seat unlock sweep, sessionsession_01KDU3qAuJyajAQm3GkUXdfA).Upstream #6191 closed 2026-08-08 01:58Z via PR #6478:
flow'sallowOrgOverridereverted tofalseper the maintainer ruling chain (#6155 Q1=B → #6283 → PR #6478), with write-side declared=enforced proven (org-scoped overlay of a packaged flow → 403NOT_OVERRIDABLE, refusal before persistence).This materially narrows the card. The body's own framing said: if the ruling is that org flow overlays should not exist, "修法是在写入侧拒绝而不是在读取侧补齐" — that write-side rejection has now shipped. Remaining scope to verify at dispatch, against current
origin/main:sys_metadataflow rows withorganization_idset (runtime create was part of the old surface — re-check after fix(spec): flow 的 allowOrgOverride 回滚为 false —— ADR-0005 原判,写路径响亮拒绝 #6478)?flow/workflow/approval一律 ❌ per-org override,注册表却写着allowOrgOverride: true——反转没有走 ADR(Prime Directive #13) #6191's ruling ordered a stored-row migration assessment for its half-1 types; whether [转移·spec] flow 的allowOrgOverride:true回滚为 false——#6155 Q1=B 裁决的契约半边,同步了结 #6191 #6283/fix(spec): flow 的 allowOrgOverride 回滚为 false —— ADR-0005 原判,写路径响亮拒绝 #6478 covered flow's stored rows needs checking.)Dispatch will be verification-first accordingly.
Generated by Claude Code
baozhoutao commentedon Aug 8, 2026
Claim: PM loop round 3 (
domain:metadataseat, sticker #6367)Session:
session_01KDU3qAuJyajAQm3GkUXdfABranch:
claude/issue-6190-org-flow-legacy-rowsWorktree:
objectstack-issue-6190Domain:
domain:metadataFile surface (verification-first; the fix half only if verification demands it):
packages/metadata-protocol/src/protocol.ts—loadMetaFromDbwhere-filter region and/orgetMetaItemsorg-record region — plus tests; possibly a legacy-row remediation whose shape is TBD by the verification. ⛔packages/runtime/src/domains/meta.tsis #3682's in-flight surface — if the fix needs it, STOP and report (serialize). ⛔ Stored-data destructive migration requires needs_decision, not implementation. Stop on breach; explain in the report.Serial constraints cleared: same-seam predecessor #4513 → PR #6561 MERGED (05:19Z); #5839 in flight (engine-core seat, protocol.ts ensureOverlayIndex region — different function region, merge-main discipline applies); #6215 → PR #6574 and #5309 → PR #6576 both ACCEPT'd and in the merge queue (no file overlap with this card); #3682 in flight (
mode:cloud, rest-server.ts + runtime meta.ts + plugin-security — this card must not touch those files); #5079 (same listing seam) deliberately NOT co-dispatched — next slot, after this card's verification lands.Container class: S/M (verification-first) —
mode:subagent, shared container.Generated by Claude Code
baozhoutao commentedon Aug 8, 2026
Verification result: the premise SURVIVES #6478 — narrowed, not closed. Draft PR #6600 ships the implementable half (the loud cold-boot log); the contract half needs a maintainer ruling and is stated at the bottom.
Session
session_01KDU3qAuJyajAQm3GkUXdfA, branchclaude/issue-6190-org-flow-legacy-rows. Everything below is measured againstorigin/mainata36db28b7with the realObjectStackProtocolImplementationover a stubsys_metadataengine, not inferred.1. Minting paths — NOT closed
PR #6478 closed the overlay tier. Re-measured, that half holds:
The runtime-create tier is open by design — PR #6478's own test says so ("a BRAND-NEW org flow still saves — allowRuntimeCreate is a different tier"), and that is ADR-0005's "a deployment, not an overlay". But that is the tier the tenant scenario in this issue actually uses: authoring a new flow in Studio, not overlaying a packaged one.
SysMetadataRepository.putstampsorganization_id: this.organizationIdfor every type, andruntime/src/domains/meta.ts(inspected read-only) still threadsresolveActiveOrganizationIdintosaveMetaItemonPUT /metadata/:type/:name. So the row is still minted today:A second path mints one too — draft +
publishPackageDraftsunder an active org:This is not a declared≠enforced hole in #6478's gate — the gate does exactly what it declares. The gap is that
allowOrgOverridegoverns whether a packaged item may be overlaid, while the row's org scoping is decided by the caller and applied to every type regardless. Closing it is a contract decision, not a bug fix; see §4.2. Legacy stored rows — the original symptom reproduces unchanged, and silently
(a) They still unbind:
(b) Nothing logs the absence — this is the measurement that mattered most:
console.warn/error/infowere all captured acrossloadMetaFromDb. Zero lines.kernel:bootstrapped's unbound audit cannot see it either, because the flow was never registered.(c) No stored-row migration shipped for flow. PR #6478's file list is: one changeset,
metadata-plugin.zod.ts, three test files, andoverlay-precedence.test.ts— no migration.packages/metadata-protocol/src/migrations/holds onlyrecorded-by-sentinelandview-definition-active-index, neither related. So #6191's migration assessment was not extended to flow's stored rows, and — per §1 — a migration alone would not settle it anyway, since new rows are still being written.3. Impact ② — real, wider than flow, filed separately as #6602
The write-through gate was not hardened; it still reads
if (this.environmentId !== undefined) return;with nothing about org. Measured on a still-org-overridable type:and the read side has the same hole (
getMetaItemshydratesorgRecordsinto the process-wide registry wheneverenvironmentId === undefined), so the TSDoc's "the write must not be more permissive about that than the read is" is no longer the right framing — both are permissive, and onlyloadMetaFromDbholds the line. Filed as #6602 (unassigned, search-first: no twin; #6483 is orthogonal — it asks which types should carry the flag, this asks what happens to the rows of the ones that legitimately do).4. What shipped, and what still needs a ruling
Shipped (draft PR #6600,
packages/metadata-protocolonly): cold boot now prints one aggregatedwarnnaming the org-scoped rows it walked past, for the types the registry declares NOT per-org overridable — with counts, a sampledname@org, the consequence ("A 'flow' listed here will NOT bind its triggers in this process") and the remedy. Silent forview-class types, where the skip is the ADR-0005 design. Hydration behaviour is unchanged. This is impact ①'s "Absence must be loud" half, and it is implementable without a ruling.Needs a ruling (three options, none guessed at):
saveMetaItemrejects an org-scoped write for any type whose registry entry isallowOrgOverride: false, so the tenant is told at the moment of the write instead of at the next restart. Contract-first and symmetric with fix(spec): flow 的 allowOrgOverride 回滚为 false —— ADR-0005 原判,写路径响亮拒绝 #6478's direction; it also removes org-scopedobject/field/hookrows, which the same path mints today, so its blast radius is much larger than flow.organization_id = NULLand binds after every restart. Preserves the capability but changes visibility — the flow becomes environment-wide, which in a multi-org deployment is a different tenancy statement than the author made.My recommendation is 1, on this project's three axes: it is the only option that keeps declared = enforced (the registry says the type has no per-org channel, so the store should not hold a per-org row of it); it makes AI-authored metadata fail loudly at authoring time rather than silently at the next restart; and it needs no business pull to justify, since it removes a surface rather than adding one. Option 2 buys back a capability nobody has been measured to want and silently re-scopes a tenant's data. But 1 is a real behaviour tightening across several types — the maintainer's call, not mine.
Generated by Claude Code
26 remaining items
fix(metadata-protocol): scan the live metadata-type registry in the c…
fix(metadata-protocol): resolve the draft's own org scope on a per-it…
fix(metadata-protocol): per-item publish re-binds runtime consumers a…
docs(adr): ADR-0126 — the platform customization model for packaged m…
docs(adr): ADR-0126 — the platform customization model for packaged m…
fix(rest): the untyped /meta/diagnostics sweep forwards the caller's …
fix(metadata-protocol): getMetaItemLayered gates the org read after t…
docs(rest): state the direction the read predicate rules on at the /p…
fix(metadata-protocol): gate `getMetaItem`'s overlay read on the meta…
rest/meta: state the caller's organization on the `/meta/diagnostics`…
fix(rest): the untyped /meta/diagnostics sweep forwards the caller's …
fix(metadata-protocol): `getMetaItemLayered` gates the org read, boun…
docs(rest): state the direction the read predicate rules on at the /p…
fix(runtime): a self-hosted restart reads the kernel's own sys_metada…
fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the re…