From 51002fab79628c3dc64fd77f750ae793b406900f Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:28:46 +0800 Subject: [PATCH 1/4] fix(coordination): bind promotion recovery policy Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../coordination/legacy_writer_fence.ts | 9 +++ .../coordination/local_authority_runtime.ts | 62 +++++++++++++++++++ .../local_authority_provider.test.ts | 4 +- .../local_authority_runtime.test.ts | 20 +++++- .../local_promotion_fixture.ts | 36 ++++++++--- 5 files changed, 117 insertions(+), 14 deletions(-) diff --git a/loopx/control_plane/coordination/legacy_writer_fence.ts b/loopx/control_plane/coordination/legacy_writer_fence.ts index 73e05b49a..124fc74f3 100644 --- a/loopx/control_plane/coordination/legacy_writer_fence.ts +++ b/loopx/control_plane/coordination/legacy_writer_fence.ts @@ -107,6 +107,12 @@ export function decodeLegacyCoordinationWriterFence(value: unknown): JsonObject fence.schema_version !== LEGACY_COORDINATION_WRITER_FENCE_SCHEMA || fence.state !== "engaged" ) throw new Error("legacy coordination writer fence must be engaged"); + const promotionPlanSha256 = fence.promotion_plan_sha256 === undefined + ? null + : requireAuthorityStoreId( + fence.promotion_plan_sha256, + "writer fence promotion plan sha256", + ); return canonicalAuthorityObject({ schema_version: LEGACY_COORDINATION_WRITER_FENCE_SCHEMA, state: "engaged", @@ -124,6 +130,9 @@ export function decodeLegacyCoordinationWriterFence(value: unknown): JsonObject fence.expected_shadow_provider_revision, "writer fence expected shadow provider revision", ), + ...(promotionPlanSha256 === null ? {} : { + promotion_plan_sha256: promotionPlanSha256, + }), }, "legacy coordination writer fence"); } diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index d76d97b18..00c468e8b 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -199,6 +199,14 @@ export async function reviewLocalCoordinationAuthorityPromotion( "qualified shadow provider revision", ); const projectionSha256 = canonicalAuthoritySha256(head); + const promotionPlanSha256 = localCoordinationPromotionPlanSha256({ + goal_id: input.goal_id, + operation_id: operationId, + expected_shadow_provider_revision: providerRevision, + expected_shadow_projection_sha256: projectionSha256, + minimum_operations: minimumOperations, + required_event_kinds: requiredEventKinds, + }); const fence = canonicalAuthorityObject({ schema_version: LEGACY_COORDINATION_WRITER_FENCE_SCHEMA, state: "engaged", @@ -207,6 +215,7 @@ export async function reviewLocalCoordinationAuthorityPromotion( source_version: `shadow:${providerRevision}`, source_projection_sha256: projectionSha256, expected_shadow_provider_revision: providerRevision, + promotion_plan_sha256: promotionPlanSha256, }, "reviewed legacy writer fence"); const request: LocalCoordinationPromotionRequest = { runtime_root: input.runtime_root, @@ -276,9 +285,12 @@ export async function reviewLocalCoordinationAuthorityPromotion( } const plan = { operation_id: operationId, + promotion_plan_sha256: promotionPlanSha256, canonical_authority: canonicalAuthority, expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: projectionSha256, + minimum_operations: minimumOperations, + required_event_kinds: [...requiredEventKinds].sort(), writer_fence: fence, rollback_identity: { provider: "file_v0", @@ -535,6 +547,42 @@ interface LocalCoordinationPromotionRequest { writer_fence: JsonObject; } +export interface LocalCoordinationPromotionPlanInput { + goal_id: string; + operation_id: string; + expected_shadow_provider_revision: string; + expected_shadow_projection_sha256: string; + minimum_operations: number; + required_event_kinds: string[]; +} + +export function localCoordinationPromotionPlanSha256( + value: LocalCoordinationPromotionPlanInput, +): string { + const plan = canonicalAuthorityObject({ + schema_version: "loopx_local_coordination_promotion_plan_v0", + goal_id: requireAuthorityStoreId(value.goal_id, "goal id"), + operation_id: requireAuthorityStoreId(value.operation_id, "operation id"), + expected_shadow_provider_revision: requireAuthorityStoreId( + value.expected_shadow_provider_revision, + "expected shadow provider revision", + ), + expected_shadow_projection_sha256: requireAuthorityStoreId( + value.expected_shadow_projection_sha256, + "expected shadow projection sha256", + ), + minimum_operations: requiredPositiveSafeInteger( + value.minimum_operations, + "minimum_operations", + ), + required_event_kinds: requiredUniqueStrings( + value.required_event_kinds, + "required_event_kinds", + ).sort(), + }, "local coordination promotion plan"); + return canonicalAuthoritySha256(plan); +} + function decodePromotionRequest(value: unknown): LocalCoordinationPromotionRequest { const input = requireJsonObject(value, "local coordination promotion request"); if (input.schema_version !== LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA) { @@ -574,6 +622,7 @@ function promotionIdentity(request: LocalCoordinationPromotionRequest): JsonObje source_projection_sha256: request.expected_shadow_projection_sha256, writer_fence_id: request.writer_fence.fence_id, source_version: request.writer_fence.source_version, + promotion_plan_sha256: localCoordinationPromotionPlanSha256(request), }, "local coordination promotion identity"); } @@ -634,6 +683,7 @@ function promotionResult( source_projection_sha256: request.expected_shadow_projection_sha256, writer_fence_id: request.writer_fence.fence_id, source_version: request.writer_fence.source_version, + promotion_plan_sha256: localCoordinationPromotionPlanSha256(request), canonical_authority: canonicalAuthority, legacy_writer_fenced: true, legacy_fallback_used: false, @@ -687,6 +737,18 @@ export async function promoteLocalCoordinationAuthority( legacy_fallback_used: false, }; } + const promotionPlanSha256 = localCoordinationPromotionPlanSha256(request); + if (request.writer_fence.promotion_plan_sha256 !== promotionPlanSha256) { + return { + schema_version: LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, + status: "failed", + reason_code: "local_authority_writer_fence_plan_mismatch", + reason: "writer fence is not bound to the complete reviewed promotion plan", + promotion_plan_sha256: promotionPlanSha256, + legacy_writer_fenced: false, + legacy_fallback_used: false, + }; + } // Provider opening can fail before durable fence readback. Report only // evidence this invocation actually verified, including in the outer catch. diff --git a/tests/control_plane_ts/local_authority_provider.test.ts b/tests/control_plane_ts/local_authority_provider.test.ts index 05db051ee..aac555c22 100644 --- a/tests/control_plane_ts/local_authority_provider.test.ts +++ b/tests/control_plane_ts/local_authority_provider.test.ts @@ -370,7 +370,9 @@ for (const provider of ["file", "sqlite"] as const) { operation_id: request.operation_id, goal_id: request.goal_id, source_shadow_provider_revision: request.expected_shadow_provider_revision, source_projection_sha256: request.expected_shadow_projection_sha256, - writer_fence_id: request.writer_fence.fence_id, source_version: request.writer_fence.source_version}; + writer_fence_id: request.writer_fence.fence_id, + source_version: request.writer_fence.source_version, + promotion_plan_sha256: request.writer_fence.promotion_plan_sha256}; const seeded = await canonical.commitAuthority({operation_id: phase === "receipt_missing" ? "other-operation" : request.operation_id, expected_provider_revision: null, next_projection: phase === "lineage_mismatch" ? {...shadow.projection, extra: "different snapshot"} : shadow.projection, diff --git a/tests/control_plane_ts/local_authority_runtime.test.ts b/tests/control_plane_ts/local_authority_runtime.test.ts index 903644e13..00182a4cb 100644 --- a/tests/control_plane_ts/local_authority_runtime.test.ts +++ b/tests/control_plane_ts/local_authority_runtime.test.ts @@ -230,7 +230,7 @@ test("local promotion fences shadow revision, digest, and writer-fence identity" minimum_operations: 2, }); assert.equal(unqualified.status, "failed"); - assert.equal(unqualified.reason_code, "local_authority_shadow_not_qualified"); + assert.equal(unqualified.reason_code, "local_authority_writer_fence_plan_mismatch"); const canonical = new FileAuthorityStore(join(root, "authority", "file-v0"), "goal-a"); assert.equal((await canonical.loadAuthority()).status, "missing"); }); @@ -281,7 +281,7 @@ test("reviewed promotion previews without effects and atomically applies the who test("reviewed promotion resumes the exact request after a fence-to-canonical interruption", async () => { const root = await mkdtemp(join(tmpdir(), "loopx-reviewed-promotion-recovery-")); - const shadow = await qualifiedShadow(root, "hard_lease"); + const shadow = await qualifiedShadow(root, "hard_lease", 2); const sourceProjection = { ...shadow.projection }; delete sourceProjection.capture_lineage_id; delete sourceProjection.capture_profile; @@ -298,7 +298,7 @@ test("reviewed promotion resumes the exact request after a fence-to-canonical in ...source, schema_version: LOCAL_COORDINATION_PROMOTION_REVIEW_REQUEST_SCHEMA, operation_id: "promote:goal-a:recoverable", - minimum_operations: 1, + minimum_operations: 2, required_event_kinds: ["todo_claim"], execute: true, }; @@ -331,6 +331,20 @@ test("reviewed promotion resumes the exact request after a fence-to-canonical in assert.equal(changed.legacy_writer_fenced, true); assert.equal((await canonical.loadAuthority()).status, "missing"); + for (const changedPolicy of [ + {...request, minimum_operations: 1}, + {...request, required_event_kinds: []}, + ]) { + const rejected = await reviewLocalCoordinationAuthorityPromotion( + changedPolicy, + dependencies, + ); + assert.equal(rejected.status, "failed", JSON.stringify(rejected)); + assert.equal(rejected.reason_code, "local_authority_writer_fence_conflict"); + assert.equal(rejected.legacy_writer_fenced, true); + assert.equal((await canonical.loadAuthority()).status, "missing"); + } + const recovered = await reviewLocalCoordinationAuthorityPromotion(request, dependencies); assert.equal(recovered.status, "recovered", JSON.stringify(recovered)); assert.equal(recovered.legacy_writer_fenced, true); diff --git a/tests/control_plane_ts/local_promotion_fixture.ts b/tests/control_plane_ts/local_promotion_fixture.ts index 10216e3bd..37db07b4f 100644 --- a/tests/control_plane_ts/local_promotion_fixture.ts +++ b/tests/control_plane_ts/local_promotion_fixture.ts @@ -3,7 +3,10 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { FileAuthorityStore } from "../../loopx/control_plane/coordination/file_authority_store.ts"; import { canonicalAuthoritySha256 as sha256 } from "../../loopx/control_plane/coordination/authority_store_codec.ts"; -import { LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA } from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; +import { + LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA, + localCoordinationPromotionPlanSha256, +} from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; import { engageLegacyCoordinationWriterFence, LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA, LEGACY_COORDINATION_WRITER_FENCE_SCHEMA } from "../../loopx/control_plane/coordination/legacy_writer_fence.ts"; import { bootstrapCoordinationRuntimeShadow, COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA } from "../../loopx/control_plane/coordination/runtime_shadow.ts"; @@ -16,7 +19,7 @@ function todoRecord(overrides: Record = {}) { } // Build a mirrored shadow fixture; this does not authorize promotion. -export async function qualifiedShadow(root: string, handoffMode = "soft_claim") { +export async function qualifiedShadow(root: string, handoffMode = "soft_claim", operationCount = 1) { const baseline = fileProjection([todoRecord()], [], handoffMode); const statePath = join(root, "ACTIVE_GOAL_STATE.md"); await writeFile(statePath, "---\ngoal_id: goal-a\nhandoff_mode: soft_claim\n---\n\n## Agent Todo\n\n"); @@ -28,11 +31,20 @@ export async function qualifiedShadow(root: string, handoffMode = "soft_claim") operation_id: "bootstrap:goal-a:state-0", source_version: "state:0", }); assert.equal(bootstrapped.status, "applied", JSON.stringify(bootstrapped)); - const entry = await pendingEntry(f, 1, {handoff_mode: handoffMode, todos: [todoRecord({claimed_by: "agent-a"})]}, - {writeClass: "todo_claim"}); - const mirrored = await commitLocalAuthorityShadowEntry(entry); - assert.equal(mirrored.outcome, "delivered", JSON.stringify(mirrored)); - await settleFiles(f, entry, mirrored); + for (let sequence = 1; sequence <= operationCount; sequence += 1) { + const entry = await pendingEntry( + f, + sequence, + {handoff_mode: handoffMode, todos: [todoRecord({ + claimed_by: "agent-a", + ...(sequence === 1 ? {} : {text: `Qualify canonical Todo semantics ${sequence}`}), + })]}, + {writeClass: sequence === 1 ? "todo_claim" : "todo_update"}, + ); + const mirrored = await commitLocalAuthorityShadowEntry(entry); + assert.equal(mirrored.outcome, "delivered", JSON.stringify(mirrored)); + await settleFiles(f, entry, mirrored); + } const loaded = await store.loadAuthority(); assert.equal(loaded.status, "loaded"); if (loaded.status !== "loaded") throw new Error("fixture head missing"); @@ -45,15 +57,18 @@ export function promotionRequest( providerRevision: string, ) { const digest = sha256(projection); - return { - schema_version: LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA, - runtime_root: root, + const plan = { goal_id: "goal-a", operation_id: "promote:goal-a:state-1", expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: digest, minimum_operations: 1, required_event_kinds: ["todo_claim"], + }; + return { + schema_version: LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA, + runtime_root: root, + ...plan, writer_fence: { schema_version: LEGACY_COORDINATION_WRITER_FENCE_SCHEMA, state: "engaged", @@ -62,6 +77,7 @@ export function promotionRequest( source_version: "state:1", source_projection_sha256: digest, expected_shadow_provider_revision: providerRevision, + promotion_plan_sha256: localCoordinationPromotionPlanSha256(plan), }, }; } From 942dbb80b8a074fe6971f7bbcb6cd569345adbb9 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:29:05 +0800 Subject: [PATCH 2/4] docs(coordination): specify promotion plan binding Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../rfcs/shared-goal-authority-state-provider-v0.md | 7 ++++++- .../rfcs/shared-goal-authority-state-provider-v0.zh-CN.md | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index fa8ad2a80..68fe97db4 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -1813,7 +1813,12 @@ It derives the compact projection from the current canonical Todo and task-lease views, reports only counts and digests, and requires `--execute` before invoking bootstrap or promotion. `promote` is effect-free without `--execute`; its preview returns the exact qualified revision, projection -digest, writer-fence identity, and rollback identity. Apply holds the shared +digest, qualification policy, canonical promotion-plan digest, writer-fence +identity, and rollback identity. The plan digest binds the Goal, operation, +exact shadow revision/projection, minimum operation count, and normalized +required event kinds; the durable fence, event, and receipt carry the same +digest, so only the exact reviewed plan can recover a fence-before-canonical +interruption. Apply holds the shared maintenance and legacy source locks while it revalidates the source snapshot, qualifies the exact shadow lineage, engages the durable writer fence, commits the canonical head, and reads back the promotion receipt. v0 rejects a Goal diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index 2e7c50c9a..be1590d1f 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -1448,7 +1448,10 @@ loopx coordination-shadow rollback --goal-id \ 它从当前 canonical Todo 与 task-lease view 派生紧凑 projection,只报告计数与摘要, 并要求 `--execute` 才调用 bootstrap 或 promotion。`promote` 未带 `--execute` 时零写入; preview 返回精确的 qualified revision、projection digest、writer-fence identity 和 -rollback identity。apply 会在同一段 maintenance 与 legacy source 锁生命周期内重新 +rollback identity,并返回 canonical promotion-plan digest 及资格策略。该 digest 会绑定 +Goal、operation、精确 shadow revision/projection、最小 operation 数与规范化后的必需 +event kind;持久 fence、event 与 receipt 都携带同一 digest,因此 fence 已落盘而 canonical +尚未提交的中断只能由完全相同的受评审 plan 恢复。apply 会在同一段 maintenance 与 legacy source 锁生命周期内重新 验证 source snapshot、资格化精确 shadow lineage、engage 持久 writer fence、提交 canonical head,并读回 promotion receipt。v0 会拒绝尚未资格化为 `hard_lease` 的 Goal, 且绝不会把 handoff mode 变化藏在 promotion 副作用中。写入成功后会立即通过 typed parity inspection From 80842b0a1d790095c636dddcbb10dfd57548fe74 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:38:39 +0800 Subject: [PATCH 3/4] fix(coordination): bind promotion target provider Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- ...shared-goal-authority-state-provider-v0.md | 3 +- ...-goal-authority-state-provider-v0.zh-CN.md | 3 +- .../coordination/local_authority_runtime.ts | 50 +++++++++++++------ .../test_sqlite_authority_cli.py | 1 + .../local_authority_provider.test.ts | 15 +++++- .../local_authority_runtime.test.ts | 31 +++++++++++- .../local_promotion_fixture.ts | 2 + 7 files changed, 85 insertions(+), 20 deletions(-) diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index 68fe97db4..5edaf40fc 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -1815,7 +1815,8 @@ before invoking bootstrap or promotion. `promote` is effect-free without `--execute`; its preview returns the exact qualified revision, projection digest, qualification policy, canonical promotion-plan digest, writer-fence identity, and rollback identity. The plan digest binds the Goal, operation, -exact shadow revision/projection, minimum operation count, and normalized +selected canonical provider, exact shadow revision/projection, minimum operation +count, and normalized required event kinds; the durable fence, event, and receipt carry the same digest, so only the exact reviewed plan can recover a fence-before-canonical interruption. Apply holds the shared diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index be1590d1f..5aa00a09d 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -1449,7 +1449,8 @@ loopx coordination-shadow rollback --goal-id \ 并要求 `--execute` 才调用 bootstrap 或 promotion。`promote` 未带 `--execute` 时零写入; preview 返回精确的 qualified revision、projection digest、writer-fence identity 和 rollback identity,并返回 canonical promotion-plan digest 及资格策略。该 digest 会绑定 -Goal、operation、精确 shadow revision/projection、最小 operation 数与规范化后的必需 +Goal、operation、选定 canonical provider、精确 shadow revision/projection、最小 +operation 数与规范化后的必需 event kind;持久 fence、event 与 receipt 都携带同一 digest,因此 fence 已落盘而 canonical 尚未提交的中断只能由完全相同的受评审 plan 恢复。apply 会在同一段 maintenance 与 legacy source 锁生命周期内重新 验证 source snapshot、资格化精确 shadow lineage、engage 持久 writer fence、提交 diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index 00c468e8b..3e2883569 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -33,6 +33,7 @@ import { } from "./coordination_projection.ts"; import { authorityStoreSourceAuthority, type AuthorityStore, type AuthorityStoreReceiptResult } from "./authority_store.ts"; import { + authorityUnicodeCompare, canonicalAuthorityBytes, canonicalAuthorityObject, canonicalAuthoritySha256, @@ -202,6 +203,7 @@ export async function reviewLocalCoordinationAuthorityPromotion( const promotionPlanSha256 = localCoordinationPromotionPlanSha256({ goal_id: input.goal_id, operation_id: operationId, + canonical_authority: canonicalAuthority, expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: projectionSha256, minimum_operations: minimumOperations, @@ -221,6 +223,7 @@ export async function reviewLocalCoordinationAuthorityPromotion( runtime_root: input.runtime_root, goal_id: input.goal_id, operation_id: operationId, + canonical_authority: canonicalAuthority, expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: projectionSha256, minimum_operations: minimumOperations, @@ -290,7 +293,7 @@ export async function reviewLocalCoordinationAuthorityPromotion( expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: projectionSha256, minimum_operations: minimumOperations, - required_event_kinds: [...requiredEventKinds].sort(), + required_event_kinds: [...requiredEventKinds].sort(authorityUnicodeCompare), writer_fence: fence, rollback_identity: { provider: "file_v0", @@ -540,6 +543,7 @@ interface LocalCoordinationPromotionRequest { runtime_root: string; goal_id: string; operation_id: string; + canonical_authority: string; expected_shadow_provider_revision: string; expected_shadow_projection_sha256: string; minimum_operations: number; @@ -550,6 +554,7 @@ interface LocalCoordinationPromotionRequest { export interface LocalCoordinationPromotionPlanInput { goal_id: string; operation_id: string; + canonical_authority: string; expected_shadow_provider_revision: string; expected_shadow_projection_sha256: string; minimum_operations: number; @@ -563,6 +568,10 @@ export function localCoordinationPromotionPlanSha256( schema_version: "loopx_local_coordination_promotion_plan_v0", goal_id: requireAuthorityStoreId(value.goal_id, "goal id"), operation_id: requireAuthorityStoreId(value.operation_id, "operation id"), + canonical_authority: requireAuthorityStoreId( + value.canonical_authority, + "canonical authority", + ), expected_shadow_provider_revision: requireAuthorityStoreId( value.expected_shadow_provider_revision, "expected shadow provider revision", @@ -578,7 +587,7 @@ export function localCoordinationPromotionPlanSha256( required_event_kinds: requiredUniqueStrings( value.required_event_kinds, "required_event_kinds", - ).sort(), + ).sort(authorityUnicodeCompare), }, "local coordination promotion plan"); return canonicalAuthoritySha256(plan); } @@ -593,6 +602,10 @@ function decodePromotionRequest(value: unknown): LocalCoordinationPromotionReque runtime_root: runtimeRoot(input.runtime_root), goal_id: requireAuthorityStoreId(input.goal_id, "goal id"), operation_id: requireAuthorityStoreId(input.operation_id, "operation id"), + canonical_authority: requireAuthorityStoreId( + input.canonical_authority, + "canonical authority", + ), expected_shadow_provider_revision: requireAuthorityStoreId( input.expected_shadow_provider_revision, "expected shadow provider revision", @@ -737,19 +750,6 @@ export async function promoteLocalCoordinationAuthority( legacy_fallback_used: false, }; } - const promotionPlanSha256 = localCoordinationPromotionPlanSha256(request); - if (request.writer_fence.promotion_plan_sha256 !== promotionPlanSha256) { - return { - schema_version: LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, - status: "failed", - reason_code: "local_authority_writer_fence_plan_mismatch", - reason: "writer fence is not bound to the complete reviewed promotion plan", - promotion_plan_sha256: promotionPlanSha256, - legacy_writer_fenced: false, - legacy_fallback_used: false, - }; - } - // Provider opening can fail before durable fence readback. Report only // evidence this invocation actually verified, including in the outer catch. let writerFenceVerified = false; @@ -763,6 +763,26 @@ export async function promoteLocalCoordinationAuthority( request.goal_id, ) ?? await openRuntimeStore(request.runtime_root, request.goal_id, dependencies); const canonicalAuthority = sourceAuthorityFor(canonical); + if (request.canonical_authority !== canonicalAuthority) return { + schema_version: LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, + status: "failed", + reason_code: "local_authority_promotion_provider_mismatch", + reason: "promotion request is not bound to the selected canonical authority provider", + expected_canonical_authority: request.canonical_authority, + observed_canonical_authority: canonicalAuthority, + legacy_writer_fenced: false, + legacy_fallback_used: false, + }; + const promotionPlanSha256 = localCoordinationPromotionPlanSha256(request); + if (request.writer_fence.promotion_plan_sha256 !== promotionPlanSha256) return { + schema_version: LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, + status: "failed", + reason_code: "local_authority_writer_fence_plan_mismatch", + reason: "writer fence is not bound to the complete reviewed promotion plan", + promotion_plan_sha256: promotionPlanSha256, + legacy_writer_fenced: false, + legacy_fallback_used: false, + }; const persistedFence = await loadLegacyCoordinationWriterFence( request.runtime_root, request.goal_id, diff --git a/tests/control_plane/test_sqlite_authority_cli.py b/tests/control_plane/test_sqlite_authority_cli.py index 447a30be6..70dda4862 100644 --- a/tests/control_plane/test_sqlite_authority_cli.py +++ b/tests/control_plane/test_sqlite_authority_cli.py @@ -121,6 +121,7 @@ def durable_bytes(): result = effect_runtime_result("coordination.local_authority.promote", { "schema_version": "loopx_local_coordination_promotion_request_v0", "runtime_root": str(runtime), "goal_id": "sqlite-goal", "operation_id": "promotion-negative", + "canonical_authority": "sqlite_v0", "expected_shadow_provider_revision": "file:synthetic:1", "expected_shadow_projection_sha256": "a" * 64, "minimum_operations": 1, "required_event_kinds": ["todo_claim"], "writer_fence": { diff --git a/tests/control_plane_ts/local_authority_provider.test.ts b/tests/control_plane_ts/local_authority_provider.test.ts index aac555c22..271b793fd 100644 --- a/tests/control_plane_ts/local_authority_provider.test.ts +++ b/tests/control_plane_ts/local_authority_provider.test.ts @@ -347,14 +347,25 @@ for (const provider of ["file", "sqlite"] as const) { const canonical = await openLocalAuthorityStore(directory, "goal-a"); const shadow = await qualifiedShadow(directory); const shadowStore = new FileAuthorityStore(join(directory, "authority-shadow", "file-v0"), "goal-a"); - const request = promotionRequest(directory, shadow.projection, shadow.providerRevision); + const canonicalAuthority = provider === "sqlite" ? "sqlite_v0" : "file_v0"; + const request = promotionRequest( + directory, + shadow.projection, + shadow.providerRevision, + canonicalAuthority, + ); if (phase === "shadow_invalid") { // A valid store row can still contain an invalid domain projection. const projection = {...shadow.projection, goal_id: "different-goal"}; const committed = await shadowStore.commitAuthority({operation_id: "invalid-domain", expected_provider_revision: shadow.providerRevision, next_projection: projection, receipts: [], events: []}); assert.equal(committed.status, "applied"); if (committed.status !== "applied") return; - Object.assign(request, promotionRequest(directory, projection, committed.provider_revision)); + Object.assign(request, promotionRequest( + directory, + projection, + committed.provider_revision, + canonicalAuthority, + )); } if (phase !== "fence_missing") await engageFence(request); const fencePath = legacyCoordinationWriterFencePath(directory, "goal-a"); diff --git a/tests/control_plane_ts/local_authority_runtime.test.ts b/tests/control_plane_ts/local_authority_runtime.test.ts index 00182a4cb..179bf4355 100644 --- a/tests/control_plane_ts/local_authority_runtime.test.ts +++ b/tests/control_plane_ts/local_authority_runtime.test.ts @@ -7,7 +7,10 @@ import test from "node:test"; import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; import { FileAuthorityStore } from "../../loopx/control_plane/coordination/file_authority_store.ts"; -import type { AuthorityStoreCommit } from "../../loopx/control_plane/coordination/authority_store.ts"; +import type { + AuthorityStore, + AuthorityStoreCommit, +} from "../../loopx/control_plane/coordination/authority_store.ts"; import { AuthorityStoreProtocolError, canonicalAuthorityBytes, @@ -225,6 +228,16 @@ test("local promotion fences shadow revision, digest, and writer-fence identity" "local_authority_writer_fence_projection_mismatch", ); + const mismatchedProvider = await promoteLocalCoordinationAuthority({ + ...request, + canonical_authority: "sqlite_v0", + }); + assert.equal(mismatchedProvider.status, "failed"); + assert.equal( + mismatchedProvider.reason_code, + "local_authority_promotion_provider_mismatch", + ); + const unqualified = await promoteLocalCoordinationAuthority({ ...request, minimum_operations: 2, @@ -331,6 +344,22 @@ test("reviewed promotion resumes the exact request after a fence-to-canonical in assert.equal(changed.legacy_writer_fenced, true); assert.equal((await canonical.loadAuthority()).status, "missing"); + const providerChangedStore: AuthorityStore = { + providerKind: "sqlite", + storeIdentity: () => canonical.storeIdentity(), + loadAuthority: () => canonical.loadAuthority(), + commitAuthority: (commit) => canonical.commitAuthority(commit), + readReceipt: (operationId) => canonical.readReceipt(operationId), + scanCommitted: (afterCursor, limit) => canonical.scanCommitted(afterCursor, limit), + }; + const providerChanged = await reviewLocalCoordinationAuthorityPromotion(request, { + createCanonicalStore: () => providerChangedStore, + }); + assert.equal(providerChanged.status, "failed", JSON.stringify(providerChanged)); + assert.equal(providerChanged.reason_code, "local_authority_writer_fence_conflict"); + assert.equal(providerChanged.legacy_writer_fenced, true); + assert.equal((await canonical.loadAuthority()).status, "missing"); + for (const changedPolicy of [ {...request, minimum_operations: 1}, {...request, required_event_kinds: []}, diff --git a/tests/control_plane_ts/local_promotion_fixture.ts b/tests/control_plane_ts/local_promotion_fixture.ts index 37db07b4f..eee48b391 100644 --- a/tests/control_plane_ts/local_promotion_fixture.ts +++ b/tests/control_plane_ts/local_promotion_fixture.ts @@ -55,11 +55,13 @@ export function promotionRequest( root: string, projection: Record, providerRevision: string, + canonicalAuthority = "file_v0", ) { const digest = sha256(projection); const plan = { goal_id: "goal-a", operation_id: "promote:goal-a:state-1", + canonical_authority: canonicalAuthority, expected_shadow_provider_revision: providerRevision, expected_shadow_projection_sha256: digest, minimum_operations: 1, From 782ee4ace4fd6172a663c441def13e6c724b48b5 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:56:50 +0800 Subject: [PATCH 4/4] ci: give minimum Node suite measured headroom Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .github/workflows/python-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 2ed266160..b7332f4b2 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -213,7 +213,9 @@ jobs: needs: changes if: needs.changes.outputs.core_tests == 'true' runs-on: ubuntu-latest - timeout-minutes: 10 + # The full public-minimum suite now runs beyond ten minutes on hosted runners. + # Keep a bounded margin above the measured workload without weakening coverage. + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6