From e015b8c97761679349aeb1b1e00a939636a0ea5f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:05:04 -0700 Subject: [PATCH] fix(orchestration): attest coordinator RPC callers --- config/reliability-gates.jsonc | 17 +++++-- ...ispatcher-orchestration-caller-evidence.ts | 23 +++++++++ src/main/runtime/rpc/dispatcher.ts | 7 ++- .../orchestration-caller-attestation.test.ts | 42 ++++++++++++++++ .../orchestration-composed-workers.test.ts | 24 ++++++---- ...on-federation-lifecycle-settlement.test.ts | 9 ++-- .../orchestration-federation-output.test.ts | 9 ++-- .../orchestration-federation-test-request.ts | 31 ++++++++++++ .../methods/orchestration-federation.test.ts | 12 +++-- ...chestration-gate-run-authorization.test.ts | 11 +++-- .../methods/orchestration-rpc-test-harness.ts | 48 ++++++++++++++++++- .../rpc/methods/orchestration-run-scope.ts | 44 ++++++++++++++--- .../rpc/methods/orchestration-runs.test.ts | 6 ++- .../runtime/rpc/methods/orchestration-runs.ts | 10 +++- ...ation-worker-start-prompt-contract.test.ts | 23 ++++++++- ...orchestration-workers-new-worktree.test.ts | 47 +++++++++++++++++- ...tion-11745-regression-verification.test.ts | 16 +++++-- ...stration-legacy-fence-jurisdiction.test.ts | 6 +-- ...estration-legacy-question-takeover.test.ts | 7 ++- .../orchestration-legacy-run-routing.test.ts | 7 +-- ...estration-legacy-takeover-delivery.test.ts | 6 +-- ...hestration-task-dispatch-invariant.test.ts | 19 +++++++- 22 files changed, 363 insertions(+), 61 deletions(-) create mode 100644 src/main/runtime/rpc/dispatcher-orchestration-caller-evidence.ts create mode 100644 src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 7abeef73e71..d608df12634 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -11891,7 +11891,7 @@ "providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local", "daemon", "ssh"], - "coverageNotes": "A deterministic isolated runtime/SQLite/RPC harness covers Run and Dispatch ownership, exact handle and remint-stable pane recipients, fixed Delivery replay and acknowledgment, restart, concurrent pollers, filtered waiters, and built-CLI parity with direct DB state. Schema v28 durably records every coordinator handle through SQLite triggers, backfills current handles on open, recreates routing triggers after old table rebuilds, and atomically canonicalizes coordinator-addressed inserts to run:. Schema v31 adds coordinator process incarnation, execution-host scope, and an authority revision; tests refuse live and unverifiable replacement, require runtime-attested restored-process continuity before backfilling a migrated revision -1 row, fence attestation loss during that bind, preserve generation and pending Delivery identity for the same process after handle/pane remint, permit only owning-host-proven exit takeover, fence a stale concurrent takeover observation, and clear stale authority when an older runtime changes only v30 binding columns. A 125-row oracle and 151-row reconciliation suite cover paged old-handle backlogs, commit-without-notify recovery, duplicate Dispatch owners, cancellation, and lifecycle fencing. Federation tests use real databases, dropped acknowledgments, restart/reconnect, and protocol v1/v2 peers; duplicate read rows do not wake again. Daemon and SSH pointer settlement is deterministic, with SSH bounded at 60 seconds beyond the 20-second mux health window. RPC changes are additive optional response fields; no stream opcode changed. Live headed/headless paired-runtime, physical SSH, WSL, Linux, and Windows acceptance remain gaps.", + "coverageNotes": "A deterministic isolated runtime/SQLite/RPC harness covers Run and Dispatch ownership, exact handle and remint-stable pane recipients, fixed Delivery replay and acknowledgment, restart, concurrent pollers, filtered waiters, and built-CLI parity with direct DB state. Schema v28 durably records every coordinator handle through SQLite triggers, backfills current handles on open, recreates routing triggers after old table rebuilds, and atomically canonicalizes coordinator-addressed inserts to run:. Schema v31 adds coordinator process incarnation, execution-host scope, and an authority revision; tests refuse live and unverifiable replacement, reject an identity-less caller that copies the live coordinator handle, require runtime-attested restored-process continuity before backfilling a migrated revision -1 row, fence attestation loss during that bind, preserve generation and pending Delivery identity for the same process after handle/pane remint, permit only owning-host-proven exit takeover, fence a stale concurrent takeover observation, and clear stale authority when an older runtime changes only v30 binding columns. A 125-row oracle and 151-row reconciliation suite cover paged old-handle backlogs, commit-without-notify recovery, duplicate Dispatch owners, cancellation, and lifecycle fencing. Federation tests use real databases, dropped acknowledgments, restart/reconnect, and protocol v1/v2 peers; duplicate read rows do not wake again. Daemon and SSH pointer settlement is deterministic, with SSH bounded at 60 seconds beyond the 20-second mux health window. RPC changes are additive optional response fields; no stream opcode changed. Live headed/headless paired-runtime, physical SSH, WSL, Linux, and Windows acceptance remain gaps.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-5729", "https://linear.app/stably/issue/STA-4325", @@ -11903,7 +11903,7 @@ "commands": [ "pnpm run build:cli && pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-message-delivery-identity.test.ts --reporter=dot --testTimeout=5000", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing-races.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-detached-routing.test.ts src/main/runtime/orchestration-mailbox-transport-settlement.test.ts src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/formatter.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/providers/ssh-pty-write.test.ts src/main/daemon/client.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/run-coordinator-authority-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/rpc/methods/orchestration-runs.test.ts src/cli/handlers/orchestration-run-cli.test.ts src/main/ipc/pty-controller-process-inventory.test.ts src/cli/handlers/orchestration-gate-cli.test.ts src/main/runtime/rpc/methods/orchestration-gates.test.ts src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/run-coordinator-authority-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/rpc/methods/orchestration-runs.test.ts src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts src/cli/handlers/orchestration-run-cli.test.ts src/main/ipc/pty-controller-process-inventory.test.ts src/cli/handlers/orchestration-gate-cli.test.ts src/main/runtime/rpc/methods/orchestration-gates.test.ts src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/main/runtime/rpc/methods/orchestration-runs.test.ts src/main/runtime/rpc/methods/orchestration-send.test.ts src/main/runtime/rpc/methods/orchestration-check.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/federation-sync.test.ts src/main/runtime/rpc/methods/orchestration-federation.test.ts src/main/runtime/rpc/methods/orchestration-federation-lifecycle-settlement.test.ts --reporter=dot" ], @@ -11931,6 +11931,7 @@ "src/main/daemon/degraded-daemon-pty-provider.test.ts", "src/main/runtime/orca-runtime.test.ts", "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts", + "src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts", "src/main/runtime/rpc/methods/orchestration-runs.test.ts", "src/main/runtime/rpc/methods/orchestration-send.test.ts", "src/main/runtime/rpc/methods/orchestration-check.test.ts", @@ -11963,6 +11964,12 @@ "explicit task listing discloses when the caller is not the current consumer" ] }, + { + "file": "src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts", + "assertions": [ + "rejects an identity-less caller that copies the current coordinator handle with no effects and agent-readable recovery steps" + ] + }, { "file": "src/main/runtime/orchestration/run-coordinator-authority-migration.test.ts", "assertions": [ @@ -12112,10 +12119,10 @@ "date": "2026-08-27", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/run-coordinator-authority-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/rpc/methods/orchestration-runs.test.ts src/cli/handlers/orchestration-run-cli.test.ts src/main/ipc/pty-controller-process-inventory.test.ts src/cli/handlers/orchestration-gate-cli.test.ts src/main/runtime/rpc/methods/orchestration-gates.test.ts src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration/run-coordinator-authority-migration.test.ts src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/rpc/methods/orchestration-runs.test.ts src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts src/cli/handlers/orchestration-run-cli.test.ts src/main/ipc/pty-controller-process-inventory.test.ts src/cli/handlers/orchestration-gate-cli.test.ts src/main/runtime/rpc/methods/orchestration-gates.test.ts src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts", "result": "passed", "durationSeconds": 5.42, - "summary": "The final focused Run-authority gate passed 95/95 across schema migration and mixed-version writes, restored-process attestation and race fencing, Delivery fencing, owning-host liveness and deadlines, coordinator lifecycle, and CLI recovery inspection. Nine authority regressions failed deterministically before their fixes; the broader orchestration, RPC, and CLI suite passed 2,405 tests with one skipped." + "summary": "The final focused Run-authority gate passed 96/96 across schema migration and mixed-version writes, caller and restored-process attestation, race and Delivery fencing, owning-host liveness and deadlines, coordinator lifecycle, and CLI recovery inspection. Ten authority regressions failed deterministically before their fixes; the final strict-attestation orchestration/RPC/CLI slice passed 1,255 tests, after an earlier broader branch gate passed 2,405 tests with one skipped." }, { "date": "2026-08-13", @@ -12164,7 +12171,7 @@ }, "redGreenEvidence": { "status": "complete", - "evidence": "The byte-identical five-test STA-4325 oracle (SHA-256 76a2d672d27e04e5b1db2f52df3b303468bdf006be7cfd003bf443618a856c35) failed 5/5 on reported v1.4.180 commit 0b62333cf179 and latest origin/main 9bb8836bb6, then passed 5/5 on its final candidate. For STA-5729, nine added regressions failed before implementation: same-pane process replacement mutated, an incarnation-less live coordinator was reported exited, an unbound migrated Run was stranded, a concurrent binding waiter was not cancelled, explicit headless Run inspection failed, targeted-local plus aggregate-local provider inventory dropped caller deadlines, and a migrated exact-handle row accepted replacement or stale restored authority; all nine passed after the authority, liveness, waiter, CLI, deadline, provenance, and revalidation fixes, followed by the 2,405-test orchestration suite with one skip. Live topology coverage remains a separate promotion gap." + "evidence": "The byte-identical five-test STA-4325 oracle (SHA-256 76a2d672d27e04e5b1db2f52df3b303468bdf006be7cfd003bf443618a856c35) failed 5/5 on reported v1.4.180 commit 0b62333cf179 and latest origin/main 9bb8836bb6, then passed 5/5 on its final candidate. For STA-5729, ten added regressions failed before implementation: same-pane process replacement mutated, an incarnation-less live coordinator was reported exited, an unbound migrated Run was stranded, a concurrent binding waiter was not cancelled, explicit headless Run inspection failed, targeted-local plus aggregate-local provider inventory dropped caller deadlines, a migrated exact-handle row accepted replacement or stale restored authority, and an identity-less caller could copy the live coordinator handle; all ten passed after the authority, liveness, waiter, CLI, deadline, provenance, attestation, and revalidation fixes. The final strict-attestation slice passed 1,255 tests after an earlier broader branch gate passed 2,405 tests with one skip. Live topology coverage remains a separate promotion gap." }, "performanceBudget": { "required": true, diff --git a/src/main/runtime/rpc/dispatcher-orchestration-caller-evidence.ts b/src/main/runtime/rpc/dispatcher-orchestration-caller-evidence.ts new file mode 100644 index 00000000000..03a89828612 --- /dev/null +++ b/src/main/runtime/rpc/dispatcher-orchestration-caller-evidence.ts @@ -0,0 +1,23 @@ +import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence' +import type { RpcRequest } from './core' + +const UNATTESTED_CALLER = Object.freeze({ paneKey: '', launchToken: '' }) + +export function resolveRpcCallerEvidence( + request: RpcRequest, + params: unknown, + legacyCoordinatorAuthority?: unknown +): OrchestrationCompatibilityEvidence | undefined { + if ( + request.orchestrationCompatibilityEvidence || + legacyCoordinatorAuthority || + !request.method.startsWith('orchestration.') + ) { + return request.orchestrationCompatibilityEvidence + } + const values = params as Record + const terminalHandle = [values.callerTerminalHandle, values.from, values.terminal].find( + (value): value is string => typeof value === 'string' + ) + return { terminalHandle: terminalHandle ?? '', ...UNATTESTED_CALLER } +} diff --git a/src/main/runtime/rpc/dispatcher.ts b/src/main/runtime/rpc/dispatcher.ts index 2ef1c235766..9bbe8d6b696 100644 --- a/src/main/runtime/rpc/dispatcher.ts +++ b/src/main/runtime/rpc/dispatcher.ts @@ -26,6 +26,7 @@ import { parseRpcRequestParams } from './dispatcher-request-parsing' import { routeDispatcherClientHostedBrowserRpc } from './dispatcher-client-browser-routing' import { needsLocalCallerFingerprint } from './dispatcher-caller-fingerprint' import { createDispatcherStreamingFeatureEmitter } from './dispatcher-streaming-feature-emitter' +import { resolveRpcCallerEvidence } from './dispatcher-orchestration-caller-evidence' export type DispatcherOptions = { runtime: OrcaRuntimeService; methods?: readonly RpcAnyMethod[] } @@ -108,6 +109,7 @@ export class RpcDispatcher { request, compatibility.legacyCoordinatorAuthority ) + const callerEvidence = resolveRpcCallerEvidence(request, effectiveParams, legacyCoordinator) const authenticatedCallerFingerprint = options?.authenticatedCallerFingerprint ?? (needsLocalCallerFingerprint(request, effectiveParams) @@ -129,7 +131,7 @@ export class RpcDispatcher { revalidateLegacyCoordinator: legacyCoordinator?.revalidate, orchestrationCompatibilityCallerAuthority: compatibility.orchestrationCompatibilityCallerAuthority, - orchestrationCompatibilityEvidence: request.orchestrationCompatibilityEvidence + orchestrationCompatibilityEvidence: callerEvidence }) } const result = await this.orchestrationMutations.run( @@ -217,6 +219,7 @@ export class RpcDispatcher { request, compatibility.legacyCoordinatorAuthority ) + const callerEvidence = resolveRpcCallerEvidence(request, effectiveParams, legacyCoordinator) const authenticatedCallerFingerprint = options?.authenticatedCallerFingerprint ?? (needsLocalCallerFingerprint(request, effectiveParams) @@ -247,7 +250,7 @@ export class RpcDispatcher { revalidateLegacyCoordinator: legacyCoordinator?.revalidate, orchestrationCompatibilityCallerAuthority: compatibility.orchestrationCompatibilityCallerAuthority, - orchestrationCompatibilityEvidence: request.orchestrationCompatibilityEvidence + orchestrationCompatibilityEvidence: callerEvidence }) } const result = await this.orchestrationMutations.run( diff --git a/src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts b/src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts new file mode 100644 index 00000000000..41bc361e46c --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration-caller-attestation.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { ORCHESTRATION_CONTRACT_VERSION } from '../../../../shared/protocol-version' +import { RpcDispatcher } from '../dispatcher' +import { createOrchestrationRpcHarness } from './orchestration-rpc-test-harness' +import { ORCHESTRATION_METHODS } from './orchestration' + +describe('orchestration coordinator caller attestation', () => { + const harness = createOrchestrationRpcHarness() + + afterEach(() => harness.cleanup()) + + it('rejects an identity-less caller that names the current coordinator handle', async () => { + const { db, runtime } = harness.setup() + const dispatcher = new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS }) + const response = await dispatcher.dispatch({ + id: 'rpc_identity-less-coordinator', + authToken: 'test-token', + method: 'orchestration.taskCreate', + orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, + params: { + spec: 'impersonated coordinator mutation', + run: db.getCurrentRunForPane(harness.coordinatorPaneKey)!.id, + callerTerminalHandle: 'term_coord' + } + }) + + expect(response).toMatchObject({ + ok: false, + error: { + code: 'consumer_fenced', + message: expect.stringContaining('authenticated identity from a live Orca agent terminal'), + data: { + effectsApplied: false, + nextSteps: expect.arrayContaining([ + expect.stringContaining('Omit --from'), + expect.stringContaining('copied terminal handle does not grant mutation authority') + ]) + } + } + }) + }) +}) diff --git a/src/main/runtime/rpc/methods/orchestration-composed-workers.test.ts b/src/main/runtime/rpc/methods/orchestration-composed-workers.test.ts index 10dbfa97a53..7925f8488fe 100644 --- a/src/main/runtime/rpc/methods/orchestration-composed-workers.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-composed-workers.test.ts @@ -56,7 +56,11 @@ describe('orchestration RPC methods', () => { exitCode: null }) vi.mocked(runtime.getTerminalProcessIncarnation).mockImplementation((handle) => - handle === 'term_worker' ? 'runtime_test:term_worker:1' : null + handle === 'term_worker' + ? 'runtime_test:term_worker:1' + : handle === 'term_coord' + ? 'runtime_test:term_coord:1' + : null ) vi.spyOn(runtime, 'getTerminalOrchestrationCliCommand').mockReturnValue('orca') vi.spyOn(runtime, 'sendTerminalAgentPrompt').mockResolvedValue({ @@ -101,7 +105,7 @@ describe('orchestration RPC methods', () => { expect(db.getDispatchContext(task.id)).toBeUndefined() }) - it('deliberately permits present but unverifiable restored-terminal evidence', async () => { + it('rejects restored-terminal evidence that names a different caller', async () => { setup() mockCurrentWorkerStart() // Restored/adopted terminals have no launch token, so verification returns null; this @@ -115,14 +119,14 @@ describe('orchestration RPC methods', () => { } } - const result = (await call('orchestration.workerStart', { - task: task.id, - from: 'term_coord', - agent: 'codex' - })) as { state: string } - - expect(result.state).toBe('ready') - expect(db.getDispatchContext(task.id)).toBeDefined() + await expect( + call('orchestration.workerStart', { + task: task.id, + from: 'term_coord', + agent: 'codex' + }) + ).rejects.toMatchObject({ code: 'consumer_fenced' }) + expect(db.getDispatchContext(task.id)).toBeUndefined() }) it('starts a fresh agent in the coordinator current worktree', async () => { diff --git a/src/main/runtime/rpc/methods/orchestration-federation-lifecycle-settlement.test.ts b/src/main/runtime/rpc/methods/orchestration-federation-lifecycle-settlement.test.ts index a54eca1fd84..47baeb78332 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation-lifecycle-settlement.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation-lifecycle-settlement.test.ts @@ -11,7 +11,10 @@ import type { OrchestrationEnvironmentTransport } from '../../orchestration/envi import { waitForFederatedLifecycleSettlement } from '../../orchestration/federation-lifecycle-settlement' import { RpcDispatcher } from '../dispatcher' import { ORCHESTRATION_METHODS } from './orchestration' -import { createFederationWorkerStartRequest as startRequest } from './orchestration-federation-test-request' +import { + createFederationWorkerStartRequest as startRequest, + mockFederationCoordinatorAttestation +} from './orchestration-federation-test-request' describe('orchestration federation lifecycle settlement', () => { let homeDb: OrchestrationDb @@ -72,9 +75,7 @@ describe('orchestration federation lifecycle settlement', () => { }) homeRuntime.setOrchestrationDb(homeDb) homeDispatcher = new RpcDispatcher({ runtime: homeRuntime, methods: ORCHESTRATION_METHODS }) - vi.spyOn(homeRuntime, 'getTerminalPaneKey').mockReturnValue( - 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' - ) + mockFederationCoordinatorAttestation(homeRuntime) configureWorkerRuntime() }) diff --git a/src/main/runtime/rpc/methods/orchestration-federation-output.test.ts b/src/main/runtime/rpc/methods/orchestration-federation-output.test.ts index adb2cc5459a..5fcf3f95ad5 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation-output.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation-output.test.ts @@ -10,6 +10,10 @@ import type { OrchestrationEnvironmentTransport } from '../../orchestration/envi import type { RpcRequest } from '../core' import { RpcDispatcher } from '../dispatcher' import { ORCHESTRATION_METHODS } from './orchestration' +import { + FEDERATION_COORDINATOR_EVIDENCE, + mockFederationCoordinatorAttestation +} from './orchestration-federation-test-request' describe('orchestration federated worker output', () => { const databases: OrchestrationDb[] = [] @@ -73,9 +77,7 @@ describe('orchestration federated worker output', () => { runtime: homeRuntime, methods: ORCHESTRATION_METHODS }) - vi.spyOn(homeRuntime, 'getTerminalPaneKey').mockImplementation((handle) => - handle === 'term_coord' ? 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' : null - ) + mockFederationCoordinatorAttestation(homeRuntime) configureWorkerRuntime(workerRuntime) }) @@ -101,6 +103,7 @@ describe('orchestration federated worker output', () => { authToken: 'coordinator-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'request_windows_worker', + orchestrationCompatibilityEvidence: FEDERATION_COORDINATOR_EVIDENCE, method: 'orchestration.workerStart', params: { task: taskId, diff --git a/src/main/runtime/rpc/methods/orchestration-federation-test-request.ts b/src/main/runtime/rpc/methods/orchestration-federation-test-request.ts index 7d004775310..8c1b8fd2ae9 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation-test-request.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation-test-request.ts @@ -1,6 +1,36 @@ +import { vi } from 'vitest' import { ORCHESTRATION_CONTRACT_VERSION } from '../../../../shared/protocol-version' +import type { OrchestrationCompatibilityEvidence } from '../../../../shared/orchestration-compatibility-evidence' +import type { OrcaRuntimeService } from '../../orca-runtime' import type { RpcRequest } from '../core' +export const FEDERATION_COORDINATOR_EVIDENCE = { + terminalHandle: 'term_coord', + paneKey: 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', + launchToken: 'coordinator-launch-token' +} as const satisfies OrchestrationCompatibilityEvidence + +export function mockFederationCoordinatorAttestation(runtime: OrcaRuntimeService): void { + vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => + handle === FEDERATION_COORDINATOR_EVIDENCE.terminalHandle + ? FEDERATION_COORDINATOR_EVIDENCE.paneKey + : null + ) + vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => + evidence?.terminalHandle === FEDERATION_COORDINATOR_EVIDENCE.terminalHandle && + evidence.paneKey === FEDERATION_COORDINATOR_EVIDENCE.paneKey + ? { + terminalHandle: evidence.terminalHandle, + paneKey: evidence.paneKey, + processIncarnation: 'home_runtime:coordinator:1', + hostScope: { kind: 'local', hostId: 'local' }, + launchTokenHash: 'test-token-hash', + terminalProvenance: 'current_runtime' + } + : null + ) +} + export function createFederationWorkerStartRequest( taskId: string, overrides: Record = {} @@ -10,6 +40,7 @@ export function createFederationWorkerStartRequest( authToken: 'coordinator-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'request_windows_worker', + orchestrationCompatibilityEvidence: FEDERATION_COORDINATOR_EVIDENCE, method: 'orchestration.workerStart', params: { task: taskId, diff --git a/src/main/runtime/rpc/methods/orchestration-federation.test.ts b/src/main/runtime/rpc/methods/orchestration-federation.test.ts index 59dd8a7a5f8..98871cb0e06 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation.test.ts @@ -9,7 +9,11 @@ import { OrchestrationDb } from '../../orchestration/db' import type { OrchestrationEnvironmentTransport } from '../../orchestration/environment-transport' import { RpcDispatcher } from '../dispatcher' import { ORCHESTRATION_METHODS } from './orchestration' -import { createFederationWorkerStartRequest as startRequest } from './orchestration-federation-test-request' +import { + createFederationWorkerStartRequest as startRequest, + FEDERATION_COORDINATOR_EVIDENCE, + mockFederationCoordinatorAttestation +} from './orchestration-federation-test-request' describe('orchestration federation', () => { const databases: OrchestrationDb[] = [] @@ -75,9 +79,7 @@ describe('orchestration federation', () => { runtime: homeRuntime, methods: ORCHESTRATION_METHODS }) - vi.spyOn(homeRuntime, 'getTerminalPaneKey').mockImplementation((handle) => - handle === 'term_coord' ? 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' : null - ) + mockFederationCoordinatorAttestation(homeRuntime) configureWorkerRuntime(workerRuntime) }) @@ -415,6 +417,7 @@ describe('orchestration federation', () => { authToken: 'coordinator-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'home_reply_request', + orchestrationCompatibilityEvidence: FEDERATION_COORDINATOR_EVIDENCE, method: 'orchestration.reply', params: { id: question!.id, @@ -472,6 +475,7 @@ describe('orchestration federation', () => { authToken: 'coordinator-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'home_late_reply_request', + orchestrationCompatibilityEvidence: FEDERATION_COORDINATOR_EVIDENCE, method: 'orchestration.reply', params: { id: questionId, body: 'yes', from: 'term_coord' } }) diff --git a/src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts b/src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts index 816ead6006c..62dc597f85c 100644 --- a/src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-gate-run-authorization.test.ts @@ -63,7 +63,7 @@ describe('orchestration gate Run authorization', () => { it('G2 refuses an unattested caller creating a gate on another Run task', async () => { const harness = createHarness() - const runA = bindCurrentCoordinatorRun(harness) + bindCurrentCoordinatorRun(harness) const before = adoptedTaskState(harness) const response = await harness.dispatcher.dispatch( @@ -78,8 +78,8 @@ describe('orchestration gate Run authorization', () => { expect(response).toMatchObject({ ok: false, error: { - code: 'task_not_found', - message: `Task ${harness.taskId} was not found in Run ${runA}.` + code: 'consumer_fenced', + data: { effectsApplied: false } } }) expect(adoptedTaskState(harness)).toEqual(before) @@ -214,7 +214,10 @@ describe('orchestration gate Run authorization', () => { ) ) - expect(response).toMatchObject({ ok: false, error: { message: `Gate not found: ${gate.id}` } }) + expect(response).toMatchObject({ + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } + }) expect(harness.db.getGate(gate.id)?.status).toBe('pending') expect(harness.db.getTask(harness.taskId)?.status).toBe('blocked') }) diff --git a/src/main/runtime/rpc/methods/orchestration-rpc-test-harness.ts b/src/main/runtime/rpc/methods/orchestration-rpc-test-harness.ts index b0a77bfa29f..070e88e2a60 100644 --- a/src/main/runtime/rpc/methods/orchestration-rpc-test-harness.ts +++ b/src/main/runtime/rpc/methods/orchestration-rpc-test-harness.ts @@ -19,6 +19,7 @@ export function createOrchestrationRpcHarness() { let dbOpen = false let runtime: OrcaRuntimeService let activeRunId: string | undefined + let defaultCtx: RpcContext const coordinatorPaneKey = COORDINATOR_PANE_KEY @@ -36,6 +37,28 @@ export function createOrchestrationRpcHarness() { vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((handle) => handle.startsWith('term_') ? `runtime_test:${handle}:1` : null ) + vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => { + const terminalHandle = evidence?.terminalHandle + const paneKey = terminalHandle ? runtime.getTerminalPaneKey(terminalHandle) : null + const terminalAuthority = terminalHandle + ? runtime.getOrchestrationDispatchAuthority(terminalHandle) + : null + const processIncarnation = terminalHandle + ? (terminalAuthority?.processIncarnation ?? + runtime.getTerminalProcessIncarnation(terminalHandle)) + : null + if (!terminalHandle || !paneKey || !processIncarnation || evidence.paneKey !== paneKey) { + return null + } + return { + terminalHandle, + paneKey, + processIncarnation, + launchTokenHash: 'test-launch-token-hash', + hostScope: terminalAuthority?.hostScope ?? { kind: 'local', hostId: 'local' }, + terminalProvenance: 'current_runtime' + } + }) if (withBoundRun) { activeRunId = db.createRun({ objective: 'Test Run', @@ -51,7 +74,8 @@ export function createOrchestrationRpcHarness() { } else { activeRunId = undefined } - return { db, runtime, ctx: { runtime }, activeRunId } + defaultCtx = { runtime } + return { db, runtime, ctx: defaultCtx, activeRunId } } function cleanup(): void { @@ -96,7 +120,27 @@ export function createOrchestrationRpcHarness() { } } const parsed = method.params ? method.params.parse(scopedParams) : undefined - return method.handler(parsed, ctx) + const callerHandle = + typeof scopedParams.callerTerminalHandle === 'string' + ? scopedParams.callerTerminalHandle + : typeof scopedParams.from === 'string' + ? scopedParams.from + : typeof scopedParams.terminal === 'string' + ? scopedParams.terminal + : undefined + const callerPaneKey = callerHandle ? runtime.getTerminalPaneKey(callerHandle) : null + const effectiveCtx = + ctx === defaultCtx && callerHandle && callerPaneKey + ? { + ...ctx, + orchestrationCompatibilityEvidence: { + terminalHandle: callerHandle, + paneKey: callerPaneKey, + launchToken: 'test-launch-token' + } + } + : ctx + return method.handler(parsed, effectiveCtx) } return { coordinatorPaneKey, setup, cleanup, findMethod, call } diff --git a/src/main/runtime/rpc/methods/orchestration-run-scope.ts b/src/main/runtime/rpc/methods/orchestration-run-scope.ts index 86b720d2ef7..bf386c90ae4 100644 --- a/src/main/runtime/rpc/methods/orchestration-run-scope.ts +++ b/src/main/runtime/rpc/methods/orchestration-run-scope.ts @@ -22,13 +22,40 @@ export type RunScopeParams = { export function assertCallerHandleMatchesEvidence( runtime: OrcaRuntimeService, callerTerminalHandle: string, - callerEvidence?: OrchestrationCompatibilityEvidence + callerEvidence?: OrchestrationCompatibilityEvidence, + options: { + callerAuthority?: OrchestrationCompatibilityCallerAuthority + allowLegacyAuthority?: boolean + } = {} ): void { - if (!callerEvidence) { + if (callerEvidence && callerEvidence.terminalHandle !== callerTerminalHandle) { + throw new OrchestrationError( + 'consumer_fenced', + `This terminal is attested as ${callerEvidence.terminalHandle} and cannot act as ${callerTerminalHandle}.`, + { effectsApplied: false } + ) + } + // Why: trusted in-process handlers omit evidence; public RPCs receive an invalid sentinel. + if (!callerEvidence || options.allowLegacyAuthority) { return } - const attested = runtime.verifyOrchestrationCompatibilityCaller(callerEvidence) - if (attested && attested.terminalHandle !== callerTerminalHandle) { + const attested = + options.callerAuthority ?? runtime.verifyOrchestrationCompatibilityCaller(callerEvidence) + if (!attested) { + throw new OrchestrationError( + 'consumer_fenced', + 'Coordinator mutations require authenticated identity from a live Orca agent terminal. No effects were applied.', + { + effectsApplied: false, + nextSteps: [ + 'Run the command inside the coordinator agent terminal with the version-matched Orca CLI.', + 'Omit --from; Orca resolves and attests the invoking agent terminal automatically.', + 'Use explicit Run reads for inspection; a copied terminal handle does not grant mutation authority.' + ] + } + ) + } + if (attested.terminalHandle !== callerTerminalHandle) { throw new OrchestrationError( 'consumer_fenced', `This terminal is attested as ${attested.terminalHandle} and cannot act as ${callerTerminalHandle}.`, @@ -65,7 +92,9 @@ export function resolveOrchestrationCaller( params: OrchestrationCallerParams ): string | null { if (!params.evidenceAssertedByCaller) { - assertCallerHandleMatchesEvidence(runtime, params.callerTerminalHandle, params.callerEvidence) + assertCallerHandleMatchesEvidence(runtime, params.callerTerminalHandle, params.callerEvidence, { + callerAuthority: params.callerAuthority + }) } const paneKey = params.callerAuthority?.terminalHandle === params.callerTerminalHandle @@ -98,10 +127,13 @@ export function resolveRunScope(runtime: OrcaRuntimeService, params: RunScopePar orchestrationSkillRecoveryData() ) } - assertCallerHandleMatchesEvidence(runtime, params.callerTerminalHandle, params.callerEvidence) if (explicit && params.legacyCoordinatorRunId === explicit.id) { + assertCallerHandleMatchesEvidence(runtime, params.callerTerminalHandle, params.callerEvidence, { + allowLegacyAuthority: true + }) return explicit } + assertCallerHandleMatchesEvidence(runtime, params.callerTerminalHandle, params.callerEvidence) const paneKey = params.callerPaneKey ?? runtime.getTerminalPaneKey(params.callerTerminalHandle) if (!paneKey) { throw new OrchestrationError( diff --git a/src/main/runtime/rpc/methods/orchestration-runs.test.ts b/src/main/runtime/rpc/methods/orchestration-runs.test.ts index 56187d9efa2..6a64d3c9246 100644 --- a/src/main/runtime/rpc/methods/orchestration-runs.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-runs.test.ts @@ -88,7 +88,11 @@ describe('orchestration RPC methods', () => { it('requires runtime-observed stable pane identity for binding', async () => { setup(false) - vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue(null) + const transientPaneKey = 'tab_stale:33333333-3333-4333-8333-333333333333' + vi.spyOn(runtime, 'getTerminalPaneKey') + .mockReturnValueOnce(transientPaneKey) + .mockReturnValueOnce(transientPaneKey) + .mockReturnValue(null) await expect( call('orchestration.runCreate', { objective: 'No pane', from: 'term_stale' }) diff --git a/src/main/runtime/rpc/methods/orchestration-runs.ts b/src/main/runtime/rpc/methods/orchestration-runs.ts index 506372c1414..ed99a4c5053 100644 --- a/src/main/runtime/rpc/methods/orchestration-runs.ts +++ b/src/main/runtime/rpc/methods/orchestration-runs.ts @@ -126,7 +126,10 @@ export const ORCHESTRATION_RUN_METHODS: RpcMethod[] = [ { effectsApplied: false } ) } - assertCallerHandleMatchesEvidence(runtime, params.from, orchestrationCompatibilityEvidence) + assertCallerHandleMatchesEvidence(runtime, params.from, orchestrationCompatibilityEvidence, { + callerAuthority, + allowLegacyAuthority: Boolean(legacyCoordinatorAuthority) + }) const db = runtime.getOrchestrationDb() const targetRun = db.getRun(params.id) const identity = resolveRunCoordinatorIdentity(runtime, params.from, paneKey) @@ -173,7 +176,10 @@ export const ORCHESTRATION_RUN_METHODS: RpcMethod[] = [ requireStablePane: true, evidenceAssertedByCaller: true }) - assertCallerHandleMatchesEvidence(runtime, params.from, orchestrationCompatibilityEvidence) + assertCallerHandleMatchesEvidence(runtime, params.from, orchestrationCompatibilityEvidence, { + callerAuthority, + allowLegacyAuthority: Boolean(legacyCoordinatorAuthority) + }) const currentIdentity = resolveRunCoordinatorIdentity(runtime, params.from, currentPaneKey) const revalidatedCaller = orchestrationCompatibilityEvidence ? runtime.verifyOrchestrationCompatibilityCaller( diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-prompt-contract.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-prompt-contract.test.ts index 826eb57ea55..3e48da0efbc 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-prompt-contract.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-prompt-contract.test.ts @@ -105,7 +105,23 @@ async function createPromptContractHarness( : null ) vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((candidate) => - candidate === handle ? `runtime_test:${handle}:1` : null + candidate === handle + ? `runtime_test:${handle}:1` + : candidate === 'term_coord' + ? 'runtime_test:term_coord:1' + : null + ) + vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => + evidence?.terminalHandle === 'term_coord' && evidence.paneKey === COORDINATOR_PANE_KEY + ? { + terminalHandle: evidence.terminalHandle, + paneKey: evidence.paneKey, + processIncarnation: 'runtime_test:term_coord:1', + hostScope: { kind: 'local', hostId: 'local' }, + launchTokenHash: 'test-token-hash', + terminalProvenance: 'current_runtime' + } + : null ) vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {}) vi.spyOn(runtime, 'showTerminal').mockResolvedValue({ @@ -139,6 +155,11 @@ async function createPromptContractHarness( authToken: 'caller-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: `${REQUEST_ID}_${outcome}`, + orchestrationCompatibilityEvidence: { + terminalHandle: 'term_coord', + paneKey: COORDINATOR_PANE_KEY, + launchToken: 'coordinator-launch-token' + }, method: 'orchestration.workerStart', params: { task: task.id, diff --git a/src/main/runtime/rpc/methods/orchestration-workers-new-worktree.test.ts b/src/main/runtime/rpc/methods/orchestration-workers-new-worktree.test.ts index 2410a74c10f..7714baf5490 100644 --- a/src/main/runtime/rpc/methods/orchestration-workers-new-worktree.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-workers-new-worktree.test.ts @@ -34,7 +34,23 @@ describe('orchestration new-worktree workers', () => { : null ) vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((handle) => - handle === 'term_worker' ? 'runtime_test:term_worker:1' : null + handle === 'term_worker' + ? 'runtime_test:term_worker:1' + : handle === 'term_coord' + ? 'runtime_test:term_coord:1' + : null + ) + vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => + evidence?.terminalHandle === 'term_coord' && evidence.paneKey === coordinatorPaneKey + ? { + terminalHandle: evidence.terminalHandle, + paneKey: evidence.paneKey, + processIncarnation: 'runtime_test:term_coord:1', + hostScope: { kind: 'local', hostId: 'local' }, + launchTokenHash: 'test-token-hash', + terminalProvenance: 'current_runtime' + } + : null ) vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {}) vi.spyOn(runtime, 'showTerminal').mockResolvedValue({ @@ -537,6 +553,11 @@ describe('orchestration new-worktree workers', () => { authToken: 'caller-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'worker_start_request', + orchestrationCompatibilityEvidence: { + terminalHandle: 'term_coord', + paneKey: coordinatorPaneKey, + launchToken: 'coordinator-launch-token' + }, method: 'orchestration.workerStart', params: { task: task.id, @@ -604,6 +625,11 @@ describe('orchestration new-worktree workers', () => { authToken: 'caller-token', orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, orchestrationRequestId: 'worker_start_request', + orchestrationCompatibilityEvidence: { + terminalHandle: 'term_coord', + paneKey: coordinatorPaneKey, + launchToken: 'coordinator-launch-token' + }, method: 'orchestration.workerStart', params: { task: task.id, @@ -632,6 +658,20 @@ describe('orchestration new-worktree workers', () => { ? `tab_coord_reminted:${coordinatorPaneKey.split(':')[1]}` : null ) + vi.spyOn(restartedRuntime, 'verifyOrchestrationCompatibilityCaller').mockImplementation( + (evidence) => + evidence?.terminalHandle === 'term_coord_reminted' && + evidence.paneKey === `tab_coord_reminted:${coordinatorPaneKey.split(':')[1]}` + ? { + terminalHandle: evidence.terminalHandle, + paneKey: evidence.paneKey, + processIncarnation: 'runtime_test:term_coord:1', + hostScope: { kind: 'local', hostId: 'local' }, + launchTokenHash: 'test-token-hash', + terminalProvenance: 'restored' + } + : null + ) const recreateWorktree = vi .spyOn(restartedRuntime, 'createManagedWorktree') .mockRejectedValue(new Error('replay recreated the worktree')) @@ -646,6 +686,11 @@ describe('orchestration new-worktree workers', () => { ...request, id: 'rpc_worker_start_retry', authToken: 'caller-token-after-restart', + orchestrationCompatibilityEvidence: { + terminalHandle: 'term_coord_reminted', + paneKey: `tab_coord_reminted:${coordinatorPaneKey.split(':')[1]}`, + launchToken: 'coordinator-launch-token-after-restart' + }, params: { ...(request.params as Record), from: 'term_coord_reminted' } }) diff --git a/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts b/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts index 47d585ca244..22a2e7ced4e 100644 --- a/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts +++ b/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts @@ -137,7 +137,10 @@ describe('#11745 H1 — gate methods authorize the caller Run', () => { ) ) - expect(response).toMatchObject({ ok: false, error: { code: 'task_not_found' } }) + expect(response).toMatchObject({ + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } + }) expect(adoptedGraph(harness)).toEqual(before) }) @@ -175,7 +178,10 @@ describe('#11745 H1 — gate methods authorize the caller Run', () => { ) ) - expect(response).toMatchObject({ ok: false, error: { message: `Gate not found: ${gate.id}` } }) + expect(response).toMatchObject({ + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } + }) expect(harness.db.getGate(gate.id)).toMatchObject({ status: 'pending', resolution: null }) expect(adoptedGraph(harness)).toEqual(before) expect(before.task).toBe('blocked') @@ -558,7 +564,7 @@ describe('#11745 H3 — adopted Run claimed by the legacy coordinator', () => { }) describe('#11745 H4 — unattested caller on a taken-over adopted Run', () => { - it('DOCUMENTS the accepted behaviour: the binding alone grants authority', async () => { + it('requires attestation after a current coordinator takes over an adopted Run', async () => { const harness = createHarness() await legacyClaimsAdoptedRun(harness, 'v-h4') await currentCoordinatorTakesOver(harness, 'v-h4') @@ -577,8 +583,8 @@ describe('#11745 H4 — unattested caller on a taken-over adopted Run', () => { ) expect(response).toMatchObject({ - ok: true, - result: { task: { run_id: harness.adoptedRunId } } + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } }) }) diff --git a/src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts b/src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts index d04c24dbbd1..30af6785f9d 100644 --- a/src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts @@ -162,7 +162,7 @@ describe('legacy coordinator fence jurisdiction', () => { expect(harness.db.listTasks({ runId: harness.adoptedRunId })).toHaveLength(1) }) - it('lets the post-takeover owner operate the adopted Run without attestation', async () => { + it('requires attestation from the post-takeover owner', async () => { const harness = createHarness() await claimAdoptedRunAsLegacyCoordinator(harness) await takeoverAdoptedRun(harness) @@ -181,8 +181,8 @@ describe('legacy coordinator fence jurisdiction', () => { ) expect(response).toMatchObject({ - ok: true, - result: { task: { run_id: harness.adoptedRunId, spec: 'owner assignment' } } + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } }) }) }) diff --git a/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts b/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts index 77d7e2d5a02..3d3ea154e7f 100644 --- a/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts @@ -279,7 +279,12 @@ describe('legacy question takeover compatibility', () => { from: CURRENT_COORDINATOR_HANDLE, run: harness.adoptedRunId }, - 'answer-after-takeover' + 'answer-after-takeover', + { + terminalHandle: CURRENT_COORDINATOR_HANDLE, + paneKey: CURRENT_COORDINATOR_PANE, + launchToken: 'current-coordinator-token' + } ) ) ).resolves.toMatchObject({ ok: true }) diff --git a/src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts b/src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts index 49e2b78d6ae..87b6cb3547c 100644 --- a/src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts @@ -43,7 +43,7 @@ describe('legacy coordinator gate run routing', () => { } ) - it('routes an unnamed Run to the caller binding even when attestation fails', async () => { + it('does not route an unnamed Run from an unattested caller', async () => { const harness = createHarness() const run = harness.db.createRun({ objective: 'current work', @@ -64,9 +64,10 @@ describe('legacy coordinator gate run routing', () => { ) expect(response).toMatchObject({ - ok: true, - result: { task: { run_id: run.id, spec: 'fresh assignment' } } + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } }) + expect(harness.db.listTasks({ runId: run.id })).toHaveLength(0) }) it('lets a current coordinator rebind the unclaimed adopted Run with actionable guidance', async () => { diff --git a/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts b/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts index 70e053d9c66..286e36dabc0 100644 --- a/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts @@ -217,10 +217,10 @@ describe('legacy coordinator delivery targets after takeover', () => { ) ) - // It still owns the Run binding, so it reads its own Run rather than being fenced read-only. + // Binding metadata alone is not caller identity after the coordinator pane changes. expect(response).toMatchObject({ - ok: true, - result: { runId: harness.adoptedRunId, legacyReadOnly: false } + ok: false, + error: { code: 'consumer_fenced', data: { effectsApplied: false } } }) }) diff --git a/src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts b/src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts index 0d194a372ba..85f7092299c 100644 --- a/src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts +++ b/src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts @@ -242,6 +242,18 @@ function createHarness(): Harness { } return null }) + vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => + evidence?.terminalHandle === COORDINATOR_HANDLE && evidence.paneKey === COORDINATOR_PANE + ? { + terminalHandle: COORDINATOR_HANDLE, + paneKey: COORDINATOR_PANE, + processIncarnation: 'runtime:coordinator:1', + hostScope: { kind: 'local', hostId: 'local' }, + launchTokenHash: 'test-token-hash', + terminalProvenance: 'current_runtime' + } + : null + ) const runId = db.createRun({ objective: 'Enforce Task/Dispatch state', coordinatorHandle: COORDINATOR_HANDLE, @@ -369,7 +381,12 @@ function request(method: string, params: Record): RpcRequest { method, params, orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION, - orchestrationRequestId: `task_dispatch_invariant_${requestSequence}` + orchestrationRequestId: `task_dispatch_invariant_${requestSequence}`, + orchestrationCompatibilityEvidence: { + terminalHandle: COORDINATOR_HANDLE, + paneKey: COORDINATOR_PANE, + launchToken: 'test-launch-token' + } } }