diff --git a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts index 9a478e01..f941cc6d 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts @@ -1,6 +1,7 @@ import { createHash } from 'crypto'; export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const; +export const OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION = 1 as const; export type OpenCodeBridgeCommandName = | 'opencode.handshake' @@ -259,6 +260,7 @@ export type OpenCodeBackfillTaskLedgerOutcome = export interface OpenCodeBackfillTaskLedgerCommandData { schemaVersion: 1; providerId: 'opencode'; + opencodeTaskLedgerEvidenceContractVersion?: number; teamName: string; taskId?: string; projectDir?: string; @@ -362,6 +364,7 @@ export interface OpenCodeBridgePeerIdentity { minVersion: number; currentVersion: number; supportedCommands: OpenCodeBridgeCommandName[]; + opencodeTaskLedgerEvidenceContractVersion?: number; }; runtime: { providerId: 'opencode'; @@ -846,7 +849,10 @@ function isPeerIdentity(value: unknown): value is OpenCodeBridgePeerIdentity { (bridgeProtocol.minVersion as number) < 1 || (bridgeProtocol.currentVersion as number) < (bridgeProtocol.minVersion as number) || !Array.isArray(bridgeProtocol.supportedCommands) || - !bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName) + !bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName) || + (bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion !== undefined && + (!Number.isInteger(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion) || + (bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1)) ) { return false; } diff --git a/test/main/services/team/OpenCodeBridgeCommandContract.test.ts b/test/main/services/team/OpenCodeBridgeCommandContract.test.ts index 48c5051c..7be4e603 100644 --- a/test/main/services/team/OpenCodeBridgeCommandContract.test.ts +++ b/test/main/services/team/OpenCodeBridgeCommandContract.test.ts @@ -6,6 +6,7 @@ import { createOpenCodeBridgeHandshakeIdentityHash, createOpenCodeBridgeIdempotencyKey, isOpenCodeBridgeCommandName, + OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION, parseSingleBridgeJsonResult, stableHash, validateBridgeResultEnvelope, @@ -202,6 +203,42 @@ describe('OpenCodeBridgeCommandContract', () => { }); }); + it('accepts handshake evidence contract version and rejects invalid values', () => { + const client = peerIdentity('claude_team'); + const server = peerIdentity('agent_teams_orchestrator'); + server.bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion = + OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION; + const validHandshake = buildHandshake({ client, server }); + + expect( + validateOpenCodeBridgeHandshake({ + handshake: validHandshake, + expectedClient: client, + requiredCommand: 'opencode.launchTeam', + expectedCapabilitySnapshotId: 'cap-1', + expectedManifestHighWatermark: 10, + expectedRunId: 'run-1', + }) + ).toEqual({ ok: true }); + + server.bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion = 0; + const invalidHandshake = buildHandshake({ client, server }); + + expect( + validateOpenCodeBridgeHandshake({ + handshake: invalidHandshake, + expectedClient: client, + requiredCommand: 'opencode.launchTeam', + expectedCapabilitySnapshotId: 'cap-1', + expectedManifestHighWatermark: 10, + expectedRunId: 'run-1', + }) + ).toEqual({ + ok: false, + reason: 'Bridge handshake peer identity is invalid', + }); + }); + it('creates deterministic idempotency keys for equivalent JSON bodies', () => { const first = createOpenCodeBridgeIdempotencyKey({ command: 'opencode.launchTeam',