From d06fe964a07c6e04c915c4a914888765677cacf2 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Thu, 8 Oct 2026 22:41:37 +0800 Subject: [PATCH] fix(control-plane): isolate host completion by Goal instance Signed-off-by: Duang777 --- .../turn_driver/host_todo_completion.ts | 14 ++++++++--- .../host_todo_completion.test.ts | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/loopx/control_plane/turn_driver/host_todo_completion.ts b/loopx/control_plane/turn_driver/host_todo_completion.ts index 5dd272d20a..709c7e637e 100644 --- a/loopx/control_plane/turn_driver/host_todo_completion.ts +++ b/loopx/control_plane/turn_driver/host_todo_completion.ts @@ -206,11 +206,17 @@ function decodeRequest( } function turnInstanceId(request: HostTodoCompletionRequest): string { + const identityParts = request.goal_instance_id === null + ? [request.goal_id, request.agent_id, request.todo_id] + : [ + "loopx_host_todo_completion_exact_v1", + request.goal_id, + request.goal_instance_id, + request.agent_id, + request.todo_id, + ]; const digest = createHash("sha256") - .update( - [request.goal_id, request.agent_id, request.todo_id].join("\0"), - "utf8", - ) + .update(identityParts.join("\0"), "utf8") .digest("hex") .slice(0, 32); return `mcp-${digest}`; diff --git a/tests/control_plane_ts/host_todo_completion.test.ts b/tests/control_plane_ts/host_todo_completion.test.ts index 459fcc7db8..02e55b8a09 100644 --- a/tests/control_plane_ts/host_todo_completion.test.ts +++ b/tests/control_plane_ts/host_todo_completion.test.ts @@ -68,6 +68,31 @@ test("host settlement commands preserve an exact GoalRef", () => { ); }); +test("host settlement identity is stable within and isolated across Goal instances", () => { + const instanceA = "ginst_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const instanceB = "ginst_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + const exact = (goalInstanceId: string) => identityFrom(prepare({ + goal_ref: {goal_id: "goal", goal_instance_id: goalInstanceId}, + })); + const firstA = exact(instanceA); + const secondA = exact(instanceA); + const identityB = exact(instanceB); + const turnInstanceIdA = `mcp-${createHash("sha256") + .update(["loopx_host_todo_completion_exact_v1", "goal", instanceA, "agent", todoId].join("\0"), "utf8") + .digest("hex") + .slice(0, 32)}`; + + assert.deepEqual(firstA, secondA); + assert.equal(firstA.turn_instance_id, turnInstanceIdA); + assert.notEqual(firstA.turn_instance_id, identityB.turn_instance_id); + assert.notEqual(firstA.effect_id, identityB.effect_id); + const crossInstance = finalize(providerOutcomes(firstA), { + goal_ref: {goal_id: "goal", goal_instance_id: instanceB}, + }); + assert.equal(crossInstance.decision, "blocked"); + assert.match(JSON.stringify(crossInstance.result), /settlement identity/); +}); + test("vision decisions require v1 and cannot combine patch with unchanged", () => { assert.throws(() => prepare({vision_path: "vision.json"}), /requires v1/); assert.throws(() => prepare({schema_version: HOST_TODO_VISION_TRANSACTION_SCHEMA_VERSION,