Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,8 @@ jobs:
needs: changes
if: needs.changes.outputs.core_tests == 'true'
runs-on: ubuntu-latest
# The full public-minimum conformance suite normally takes about 9-10
# minutes on hosted runners. Keep enough headroom for setup and cleanup
# without dropping compatibility cases or weakening their deadlines.
# 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1813,7 +1813,13 @@ 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,
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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,11 @@ loopx coordination-shadow rollback --goal-id <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、选定 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、提交
canonical head,并读回 promotion receipt。v0 会拒绝尚未资格化为 `hard_lease` 的 Goal,
且绝不会把 handoff mode 变化藏在 promotion 副作用中。写入成功后会立即通过 typed parity inspection
Expand Down
9 changes: 9 additions & 0 deletions loopx/control_plane/coordination/legacy_writer_fence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
}

Expand Down
84 changes: 83 additions & 1 deletion loopx/control_plane/coordination/local_authority_runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from "./coordination_projection.ts";
import { authorityStoreSourceAuthority, type AuthorityStore, type AuthorityStoreReceiptResult } from "./authority_store.ts";
import {
authorityUnicodeCompare,
canonicalAuthorityBytes,
canonicalAuthorityObject,
canonicalAuthoritySha256,
Expand Down Expand Up @@ -199,6 +200,15 @@ export async function reviewLocalCoordinationAuthorityPromotion(
"qualified shadow provider revision",
);
const projectionSha256 = canonicalAuthoritySha256(head);
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,
required_event_kinds: requiredEventKinds,
});
const fence = canonicalAuthorityObject({
schema_version: LEGACY_COORDINATION_WRITER_FENCE_SCHEMA,
state: "engaged",
Expand All @@ -207,11 +217,13 @@ 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,
goal_id: input.goal_id,
operation_id: operationId,
canonical_authority: canonicalAuthority,
expected_shadow_provider_revision: providerRevision,
expected_shadow_projection_sha256: projectionSha256,
minimum_operations: minimumOperations,
Expand Down Expand Up @@ -276,9 +288,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(authorityUnicodeCompare),
writer_fence: fence,
rollback_identity: {
provider: "file_v0",
Expand Down Expand Up @@ -528,13 +543,55 @@ 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;
required_event_kinds: string[];
writer_fence: JsonObject;
}

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;
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"),
canonical_authority: requireAuthorityStoreId(
value.canonical_authority,
"canonical authority",
),
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(authorityUnicodeCompare),
}, "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) {
Expand All @@ -545,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",
Expand Down Expand Up @@ -574,6 +635,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");
}

Expand Down Expand Up @@ -634,6 +696,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,
Expand Down Expand Up @@ -687,7 +750,6 @@ export async function promoteLocalCoordinationAuthority(
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;
Expand All @@ -701,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,
Expand Down
1 change: 1 addition & 0 deletions tests/control_plane/test_sqlite_authority_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
19 changes: 16 additions & 3 deletions tests/control_plane_ts/local_authority_provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -370,7 +381,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,
Expand Down
51 changes: 47 additions & 4 deletions tests/control_plane_ts/local_authority_runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -225,12 +228,22 @@ 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,
});
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");
});
Expand Down Expand Up @@ -281,7 +294,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;
Expand All @@ -298,7 +311,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,
};
Expand Down Expand Up @@ -331,6 +344,36 @@ 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: []},
]) {
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);
Expand Down
Loading
Loading