feat(opencode): add team runtime integration

This commit is contained in:
777genius 2026-04-21 20:22:27 +03:00
parent 2e87e12774
commit 5e31bd1c06
117 changed files with 23210 additions and 178 deletions

View file

@ -218,6 +218,29 @@ function shouldWaitForStop(flags = {}) {
return true;
}
function compactRuntimeToolBody(context, flags = {}, fields) {
const body = { teamName: context.teamName };
for (const field of fields) {
if (flags[field] !== undefined) {
body[field] = flags[field];
}
}
return body;
}
async function postRuntimeTool(context, flags = {}, toolPath, body) {
const baseUrls = resolveControlBaseUrls(context, flags);
return requestJsonWithFallback(
baseUrls,
`/api/teams/${encodeURIComponent(context.teamName)}/opencode/runtime/${toolPath}`,
{
method: 'POST',
body,
timeoutMs: normalizeTimeoutMs(flags.waitTimeoutMs || flags['wait-timeout-ms'] || 10000),
}
);
}
async function waitForProvisioningState(baseUrls, teamName, runId, timeoutMs) {
const startedAt = Date.now();
let lastProgress = null;
@ -331,8 +354,82 @@ async function getRuntimeState(context, flags = {}) {
return requestJsonWithFallback(baseUrls, `/api/teams/${encodeURIComponent(context.teamName)}/runtime`);
}
async function runtimeBootstrapCheckin(context, flags = {}) {
return postRuntimeTool(
context,
flags,
'bootstrap-checkin',
compactRuntimeToolBody(context, flags, [
'runId',
'memberName',
'runtimeSessionId',
'observedAt',
'diagnostics',
'metadata',
])
);
}
async function runtimeDeliverMessage(context, flags = {}) {
return postRuntimeTool(
context,
flags,
'deliver-message',
compactRuntimeToolBody(context, flags, [
'idempotencyKey',
'runId',
'fromMemberName',
'runtimeSessionId',
'to',
'text',
'createdAt',
'summary',
'taskRefs',
])
);
}
async function runtimeTaskEvent(context, flags = {}) {
return postRuntimeTool(
context,
flags,
'task-event',
compactRuntimeToolBody(context, flags, [
'idempotencyKey',
'runId',
'memberName',
'runtimeSessionId',
'taskId',
'event',
'createdAt',
'summary',
'metadata',
])
);
}
async function runtimeHeartbeat(context, flags = {}) {
return postRuntimeTool(
context,
flags,
'heartbeat',
compactRuntimeToolBody(context, flags, [
'runId',
'memberName',
'runtimeSessionId',
'observedAt',
'status',
'metadata',
])
);
}
module.exports = {
launchTeam,
stopTeam,
getRuntimeState,
runtimeBootstrapCheckin,
runtimeDeliverMessage,
runtimeTaskEvent,
runtimeHeartbeat,
};

View file

@ -51,7 +51,14 @@ const AGENT_TEAMS_KANBAN_TOOL_NAMES = [
'kanban_set_column',
];
const AGENT_TEAMS_RUNTIME_TOOL_NAMES = ['team_launch', 'team_stop'];
const AGENT_TEAMS_RUNTIME_TOOL_NAMES = [
'team_launch',
'team_stop',
'runtime_bootstrap_checkin',
'runtime_deliver_message',
'runtime_task_event',
'runtime_heartbeat',
];
const AGENT_TEAMS_MCP_TOOL_GROUPS = [
{

View file

@ -1069,6 +1069,77 @@ describe('agent-teams-controller API', () => {
}
});
it('forwards OpenCode runtime MCP calls to the app control API', async () => {
const claudeDir = makeClaudeDir();
const controller = createController({ teamName: 'my-team', claudeDir });
const calls = [];
const server = await startControlServer(async ({ method, url, body }) => {
calls.push({ method, url, body });
if (method === 'POST' && url === '/api/teams/my-team/opencode/runtime/bootstrap-checkin') {
return { body: { ok: true, state: 'accepted' } };
}
if (method === 'POST' && url === '/api/teams/my-team/opencode/runtime/deliver-message') {
return { body: { ok: true, state: 'delivered' } };
}
if (method === 'POST' && url === '/api/teams/my-team/opencode/runtime/task-event') {
return { body: { ok: true, state: 'recorded' } };
}
if (method === 'POST' && url === '/api/teams/my-team/opencode/runtime/heartbeat') {
return { body: { ok: true, state: 'accepted' } };
}
return { statusCode: 404, body: { error: `Unhandled ${method} ${url}` } };
});
try {
await controller.runtime.runtimeBootstrapCheckin({
controlUrl: server.baseUrl,
runId: 'run-oc',
memberName: 'bob',
runtimeSessionId: 'ses-1',
});
await controller.runtime.runtimeDeliverMessage({
controlUrl: server.baseUrl,
idempotencyKey: 'idem-1',
runId: 'run-oc',
fromMemberName: 'bob',
runtimeSessionId: 'ses-1',
to: 'user',
text: 'hello',
});
await controller.runtime.runtimeTaskEvent({
controlUrl: server.baseUrl,
idempotencyKey: 'idem-task-1',
runId: 'run-oc',
memberName: 'bob',
runtimeSessionId: 'ses-1',
taskId: 'task-1',
event: 'started',
});
await controller.runtime.runtimeHeartbeat({
controlUrl: server.baseUrl,
runId: 'run-oc',
memberName: 'bob',
runtimeSessionId: 'ses-1',
});
expect(calls.map((call) => call.url)).toEqual([
'/api/teams/my-team/opencode/runtime/bootstrap-checkin',
'/api/teams/my-team/opencode/runtime/deliver-message',
'/api/teams/my-team/opencode/runtime/task-event',
'/api/teams/my-team/opencode/runtime/heartbeat',
]);
expect(calls[0].body).toEqual({
teamName: 'my-team',
runId: 'run-oc',
memberName: 'bob',
runtimeSessionId: 'ses-1',
});
} finally {
await server.close();
}
});
it('prefers the published control endpoint over a stale env URL', async () => {
const claudeDir = makeClaudeDir();
const controller = createController({ teamName: 'my-team', claudeDir });

View file

@ -76,6 +76,10 @@ declare module 'agent-teams-controller' {
launchTeam(flags: Record<string, unknown>): Promise<unknown>;
stopTeam(flags?: Record<string, unknown>): Promise<unknown>;
getRuntimeState(flags?: Record<string, unknown>): Promise<unknown>;
runtimeBootstrapCheckin(flags: Record<string, unknown>): Promise<unknown>;
runtimeDeliverMessage(flags: Record<string, unknown>): Promise<unknown>;
runtimeTaskEvent(flags: Record<string, unknown>): Promise<unknown>;
runtimeHeartbeat(flags: Record<string, unknown>): Promise<unknown>;
}
export interface AgentTeamsController {

View file

@ -11,6 +11,22 @@ const toolContextSchema = {
waitTimeoutMs: z.number().int().min(1000).max(600000).optional(),
};
const runtimeMetadataSchema = z.record(z.string(), z.unknown()).optional();
const runtimeDiagnosticsSchema = z.array(z.string().min(1)).optional();
const runtimeIdentitySchema = {
...toolContextSchema,
runId: z.string().min(1),
memberName: z.string().min(1),
runtimeSessionId: z.string().min(1),
};
const runtimeDeliveryTargetSchema = z.union([
z.literal('user'),
z.object({
memberName: z.string().min(1),
teamName: z.string().min(1).optional(),
}),
]);
export function registerRuntimeTools(server: Pick<FastMCP, 'addTool'>) {
server.addTool({
name: 'team_launch',
@ -75,4 +91,168 @@ export function registerRuntimeTools(server: Pick<FastMCP, 'addTool'>) {
})
),
});
server.addTool({
name: 'runtime_bootstrap_checkin',
description: 'Confirm that an OpenCode team member runtime reached the app MCP bootstrap boundary',
parameters: z.object({
...runtimeIdentitySchema,
observedAt: z.string().min(1).optional(),
diagnostics: runtimeDiagnosticsSchema,
metadata: runtimeMetadataSchema,
}),
execute: async ({
teamName,
claudeDir,
controlUrl,
waitTimeoutMs,
runId,
memberName,
runtimeSessionId,
observedAt,
diagnostics,
metadata,
}) =>
jsonTextContent(
await getController(teamName, claudeDir).runtime.runtimeBootstrapCheckin({
runId,
memberName,
runtimeSessionId,
...(observedAt ? { observedAt } : {}),
...(diagnostics ? { diagnostics } : {}),
...(metadata ? { metadata } : {}),
...(controlUrl ? { controlUrl } : {}),
...(waitTimeoutMs ? { waitTimeoutMs } : {}),
})
),
});
server.addTool({
name: 'runtime_deliver_message',
description: 'Deliver an OpenCode runtime message to the app-owned team journal and destination',
parameters: z.object({
...toolContextSchema,
idempotencyKey: z.string().min(1),
runId: z.string().min(1),
fromMemberName: z.string().min(1),
runtimeSessionId: z.string().min(1),
to: runtimeDeliveryTargetSchema,
text: z.string().min(1),
createdAt: z.string().min(1).optional(),
summary: z.string().optional(),
taskRefs: z.array(z.unknown()).optional(),
}),
execute: async ({
teamName,
claudeDir,
controlUrl,
waitTimeoutMs,
idempotencyKey,
runId,
fromMemberName,
runtimeSessionId,
to,
text,
createdAt,
summary,
taskRefs,
}) =>
jsonTextContent(
await getController(teamName, claudeDir).runtime.runtimeDeliverMessage({
idempotencyKey,
runId,
fromMemberName,
runtimeSessionId,
to,
text,
...(createdAt ? { createdAt } : {}),
...(summary ? { summary } : {}),
...(taskRefs ? { taskRefs } : {}),
...(controlUrl ? { controlUrl } : {}),
...(waitTimeoutMs ? { waitTimeoutMs } : {}),
})
),
});
server.addTool({
name: 'runtime_task_event',
description: 'Record an idempotent OpenCode runtime task event for app-side attribution',
parameters: z.object({
...toolContextSchema,
idempotencyKey: z.string().min(1),
runId: z.string().min(1),
memberName: z.string().min(1),
runtimeSessionId: z.string().min(1).optional(),
taskId: z.string().min(1),
event: z.string().min(1),
createdAt: z.string().min(1).optional(),
summary: z.string().optional(),
metadata: runtimeMetadataSchema,
}),
execute: async ({
teamName,
claudeDir,
controlUrl,
waitTimeoutMs,
idempotencyKey,
runId,
memberName,
runtimeSessionId,
taskId,
event,
createdAt,
summary,
metadata,
}) =>
jsonTextContent(
await getController(teamName, claudeDir).runtime.runtimeTaskEvent({
idempotencyKey,
runId,
memberName,
...(runtimeSessionId ? { runtimeSessionId } : {}),
taskId,
event,
...(createdAt ? { createdAt } : {}),
...(summary ? { summary } : {}),
...(metadata ? { metadata } : {}),
...(controlUrl ? { controlUrl } : {}),
...(waitTimeoutMs ? { waitTimeoutMs } : {}),
})
),
});
server.addTool({
name: 'runtime_heartbeat',
description: 'Refresh OpenCode member runtime liveness in the app-owned launch state',
parameters: z.object({
...runtimeIdentitySchema,
observedAt: z.string().min(1).optional(),
status: z.enum(['alive', 'idle', 'busy']).optional(),
metadata: runtimeMetadataSchema,
}),
execute: async ({
teamName,
claudeDir,
controlUrl,
waitTimeoutMs,
runId,
memberName,
runtimeSessionId,
observedAt,
status,
metadata,
}) =>
jsonTextContent(
await getController(teamName, claudeDir).runtime.runtimeHeartbeat({
runId,
memberName,
runtimeSessionId,
...(observedAt ? { observedAt } : {}),
...(status ? { status } : {}),
...(metadata ? { metadata } : {}),
...(controlUrl ? { controlUrl } : {}),
...(waitTimeoutMs ? { waitTimeoutMs } : {}),
})
),
});
}

View file

@ -215,6 +215,69 @@ describe('agent-teams-mcp tools', () => {
}
});
it('forwards OpenCode runtime MCP tools through the runtime control bridge', async () => {
const calls: Array<{ method?: string; url?: string; body?: unknown }> = [];
const server = await startControlServer(async ({ method, url, body }) => {
calls.push({ method, url, body });
return { body: { ok: true, state: 'accepted' } };
});
try {
await getTool('runtime_bootstrap_checkin').execute({
teamName: 'alpha',
controlUrl: server.baseUrl,
runId: 'run-oc',
memberName: 'alice',
runtimeSessionId: 'ses-1',
});
await getTool('runtime_deliver_message').execute({
teamName: 'alpha',
controlUrl: server.baseUrl,
idempotencyKey: 'idem-1',
runId: 'run-oc',
fromMemberName: 'alice',
runtimeSessionId: 'ses-1',
to: 'user',
text: 'hello',
});
await getTool('runtime_task_event').execute({
teamName: 'alpha',
controlUrl: server.baseUrl,
idempotencyKey: 'idem-task-1',
runId: 'run-oc',
memberName: 'alice',
runtimeSessionId: 'ses-1',
taskId: 'task-1',
event: 'started',
});
await getTool('runtime_heartbeat').execute({
teamName: 'alpha',
controlUrl: server.baseUrl,
runId: 'run-oc',
memberName: 'alice',
runtimeSessionId: 'ses-1',
});
expect(calls.map((call) => call.url)).toEqual([
'/api/teams/alpha/opencode/runtime/bootstrap-checkin',
'/api/teams/alpha/opencode/runtime/deliver-message',
'/api/teams/alpha/opencode/runtime/task-event',
'/api/teams/alpha/opencode/runtime/heartbeat',
]);
expect(calls[1].body).toEqual({
teamName: 'alpha',
idempotencyKey: 'idem-1',
runId: 'run-oc',
fromMemberName: 'alice',
runtimeSessionId: 'ses-1',
to: 'user',
text: 'hello',
});
} finally {
await server.close();
}
});
it('discovers the control endpoint from the published state file', async () => {
const claudeDir = makeClaudeDir();
const statePath = path.join(claudeDir, 'team-control-api.json');

View file

@ -6,6 +6,7 @@ import {
} from '@shared/utils/effortLevels';
import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isTeamProviderId } from '@shared/utils/teamProvider';
import { isAbsolute } from 'path';
import type { HttpServices } from './index';
@ -33,6 +34,9 @@ function getStatusCode(error: unknown, fallback: number = 500): number {
if (error instanceof HttpFeatureUnavailableError) {
return 501;
}
if (error instanceof Error && error.name === 'RuntimeStaleEvidenceError') {
return 409;
}
return fallback;
}
@ -110,15 +114,15 @@ function assertOptionalFastMode(value: unknown): TeamFastMode | undefined {
function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest {
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
const providerId =
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: payload.providerId == null || payload.providerId === 'anthropic'
? 'anthropic'
: (() => {
throw new HttpBadRequestError('providerId must be anthropic, codex, or gemini');
})();
payload.providerId == null
? 'anthropic'
: isTeamProviderId(payload.providerId)
? payload.providerId
: (() => {
throw new HttpBadRequestError(
'providerId must be anthropic, codex, gemini, or opencode'
);
})();
const prompt = assertOptionalString(payload.prompt, 'prompt');
const rawProviderBackendId = assertOptionalString(payload.providerBackendId, 'providerBackendId');
const providerBackendId = migrateProviderBackendId(providerId, rawProviderBackendId);
@ -169,6 +173,18 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
};
}
function withRuntimeTeamName(teamName: string, body: unknown): Record<string, unknown> {
const payload =
body && typeof body === 'object' && !Array.isArray(body)
? (body as Record<string, unknown>)
: {};
const bodyTeamName = typeof payload.teamName === 'string' ? payload.teamName.trim() : '';
if (bodyTeamName && bodyTeamName !== teamName) {
throw new HttpBadRequestError('runtime body teamName must match route teamName');
}
return { ...payload, teamName };
}
export function registerTeamRoutes(app: FastifyInstance, services: HttpServices): void {
app.post<{ Params: { teamName: string }; Body: LaunchBody }>(
'/api/teams/:teamName/launch',
@ -283,4 +299,104 @@ export function registerTeamRoutes(app: FastifyInstance, services: HttpServices)
return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) });
}
});
app.post<{ Params: { teamName: string }; Body: Record<string, unknown> }>(
'/api/teams/:teamName/opencode/runtime/bootstrap-checkin',
async (request, reply) => {
try {
const validatedTeamName = validateTeamName(request.params.teamName);
if (!validatedTeamName.valid) {
return reply.status(400).send({ error: validatedTeamName.error });
}
return reply.send(
await getTeamProvisioningService(services).recordOpenCodeRuntimeBootstrapCheckin(
withRuntimeTeamName(validatedTeamName.value!, request.body)
)
);
} catch (error) {
if (shouldLogError(error)) {
logger.error(
`Error in POST /api/teams/${request.params.teamName}/opencode/runtime/bootstrap-checkin:`,
getErrorMessage(error)
);
}
return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) });
}
}
);
app.post<{ Params: { teamName: string }; Body: Record<string, unknown> }>(
'/api/teams/:teamName/opencode/runtime/deliver-message',
async (request, reply) => {
try {
const validatedTeamName = validateTeamName(request.params.teamName);
if (!validatedTeamName.valid) {
return reply.status(400).send({ error: validatedTeamName.error });
}
return reply.send(
await getTeamProvisioningService(services).deliverOpenCodeRuntimeMessage(
withRuntimeTeamName(validatedTeamName.value!, request.body)
)
);
} catch (error) {
if (shouldLogError(error)) {
logger.error(
`Error in POST /api/teams/${request.params.teamName}/opencode/runtime/deliver-message:`,
getErrorMessage(error)
);
}
return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) });
}
}
);
app.post<{ Params: { teamName: string }; Body: Record<string, unknown> }>(
'/api/teams/:teamName/opencode/runtime/task-event',
async (request, reply) => {
try {
const validatedTeamName = validateTeamName(request.params.teamName);
if (!validatedTeamName.valid) {
return reply.status(400).send({ error: validatedTeamName.error });
}
return reply.send(
await getTeamProvisioningService(services).recordOpenCodeRuntimeTaskEvent(
withRuntimeTeamName(validatedTeamName.value!, request.body)
)
);
} catch (error) {
if (shouldLogError(error)) {
logger.error(
`Error in POST /api/teams/${request.params.teamName}/opencode/runtime/task-event:`,
getErrorMessage(error)
);
}
return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) });
}
}
);
app.post<{ Params: { teamName: string }; Body: Record<string, unknown> }>(
'/api/teams/:teamName/opencode/runtime/heartbeat',
async (request, reply) => {
try {
const validatedTeamName = validateTeamName(request.params.teamName);
if (!validatedTeamName.valid) {
return reply.status(400).send({ error: validatedTeamName.error });
}
return reply.send(
await getTeamProvisioningService(services).recordOpenCodeRuntimeHeartbeat(
withRuntimeTeamName(validatedTeamName.value!, request.body)
)
);
} catch (error) {
if (shouldLogError(error)) {
logger.error(
`Error in POST /api/teams/${request.params.teamName}/opencode/runtime/heartbeat:`,
getErrorMessage(error)
);
}
return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) });
}
}
);
}

View file

@ -47,7 +47,10 @@ import { ReviewApplierService } from '@main/services/team/ReviewApplierService';
import { TeamBackupService } from '@main/services/team/TeamBackupService';
import { TeamConfigReader } from '@main/services/team/TeamConfigReader';
import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter';
import { TeamMcpConfigBuilder } from '@main/services/team/TeamMcpConfigBuilder';
import {
resolveAgentTeamsMcpLaunchSpec,
TeamMcpConfigBuilder,
} from '@main/services/team/TeamMcpConfigBuilder';
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService';
import {
@ -112,6 +115,18 @@ import {
type TeamReconcileTrigger,
} from './services/team/TeamReconcileDrainScheduler';
import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore';
import { OpenCodeBridgeCommandClient } from './services/team/opencode/bridge/OpenCodeBridgeCommandClient';
import {
createOpenCodeBridgeCommandLeaseStore,
createOpenCodeBridgeCommandLedgerStore,
} from './services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore';
import {
createOpenCodeBridgeClientIdentity,
OpenCodeBridgeCommandHandshakePort,
} from './services/team/opencode/bridge/OpenCodeBridgeHandshakeClient';
import { OpenCodeStateChangingBridgeCommandService } from './services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
import { OpenCodeProductionE2EEvidenceStore } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore';
import { OpenCodeRuntimeManifestEvidenceReader } from './services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader';
import { getAppIconPath } from './utils/appIcon';
import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder';
import {
@ -130,11 +145,14 @@ import {
BoardTaskExactLogsService,
BoardTaskLogStreamService,
BranchStatusService,
ClaudeBinaryResolver,
CliInstallerService,
configManager,
LocalFileSystemProvider,
MemberStatsComputer,
NotificationManager,
OpenCodeReadinessBridge,
OpenCodeTeamRuntimeAdapter,
PtyTerminalService,
ServiceContext,
ServiceContextRegistry,
@ -142,6 +160,7 @@ import {
TaskBoundaryParser,
TeamDataService,
TeamLogSourceTracker,
TeamRuntimeAdapterRegistry,
TeamTaskStallJournal,
TeamTaskStallMonitor,
TeamTaskStallNotifier,
@ -155,6 +174,7 @@ import {
import type { FileChangeEvent } from '@main/types';
import type { TeamChangeEvent } from '@shared/types';
import type { OpenCodeTeamLaunchMode } from './services/team';
const logger = createLogger('App');
startEventLoopLagMonitor();
@ -179,6 +199,83 @@ const INBOX_NOTIFY_DEBOUNCE_MS = 500;
/** Messages sent from our UI (user_sent) — suppress notifications for these. */
const suppressedSources = new Set(['user_sent']);
function resolveOpenCodeTeamLaunchModeFromEnv(): OpenCodeTeamLaunchMode {
const raw = process.env.CLAUDE_TEAM_OPENCODE_LAUNCH_MODE?.trim().toLowerCase();
if (raw === 'dogfood' || raw === 'production' || raw === 'disabled') {
return raw;
}
if (process.env.CLAUDE_TEAM_OPENCODE_DOGFOOD === '1') {
return 'dogfood';
}
return 'disabled';
}
async function createOpenCodeRuntimeAdapterRegistry(): Promise<TeamRuntimeAdapterRegistry> {
const binaryPath = await ClaudeBinaryResolver.resolve();
if (!binaryPath) {
logger.warn('[OpenCode] Runtime adapter bridge disabled: orchestrator CLI binary not resolved');
return new TeamRuntimeAdapterRegistry();
}
const bridgeEnv = { ...process.env };
try {
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
const mcpEntry = mcpLaunchSpec.args[0];
if (mcpEntry) {
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = mcpLaunchSpec.command;
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = mcpEntry;
}
} catch (error) {
logger.warn(
`[OpenCode] Runtime adapter bridge MCP entrypoint unresolved: ${
error instanceof Error ? error.message : String(error)
}`
);
}
const bridgeClient = new OpenCodeBridgeCommandClient({
binaryPath,
tempDirectory: join(app.getPath('temp'), 'claude-team-opencode-bridge'),
env: bridgeEnv,
});
const bridgeControlDir = join(app.getPath('userData'), 'opencode-bridge');
const clientIdentity = createOpenCodeBridgeClientIdentity({
appVersion: typeof app.getVersion === 'function' ? app.getVersion() : '1.3.0',
gitSha: process.env.VITE_GIT_SHA ?? process.env.GIT_SHA ?? null,
buildId: process.env.VITE_BUILD_ID ?? process.env.BUILD_ID ?? null,
});
const stateChangingCommands = new OpenCodeStateChangingBridgeCommandService({
expectedClientIdentity: clientIdentity,
handshakePort: new OpenCodeBridgeCommandHandshakePort({
bridge: bridgeClient,
clientIdentity,
}),
leaseStore: createOpenCodeBridgeCommandLeaseStore({
filePath: join(bridgeControlDir, 'command-leases.json'),
}),
ledger: createOpenCodeBridgeCommandLedgerStore({
filePath: join(bridgeControlDir, 'command-ledger.json'),
}),
bridge: bridgeClient,
manifestReader: new OpenCodeRuntimeManifestEvidenceReader({
teamsBasePath: getTeamsBasePath(),
}),
});
return new TeamRuntimeAdapterRegistry([
new OpenCodeTeamRuntimeAdapter(
new OpenCodeReadinessBridge(bridgeClient, {
stateChangingCommands,
productionE2eEvidence: new OpenCodeProductionE2EEvidenceStore({
filePath: join(bridgeControlDir, 'production-e2e-evidence.json'),
}),
}),
{
launchMode: resolveOpenCodeTeamLaunchModeFromEnv(),
}
),
]);
}
// --- Team display name cache (avoid listTeams() on every notification) ---
const TEAM_DISPLAY_NAME_TTL_MS = 30_000;
const teamDisplayNameCache = new Map<string, { value: string; expiresAt: number }>();
@ -838,6 +935,7 @@ async function initializeServices(): Promise<void> {
teamDataService = new TeamDataService();
teamDataService.setMemberRuntimeAdvisoryService(teamMemberRuntimeAdvisoryService);
teamProvisioningService = new TeamProvisioningService();
teamProvisioningService.setRuntimeAdapterRegistry(await createOpenCodeRuntimeAdapterRegistry());
// Startup GC: remove stale MCP config files from previous sessions (best-effort)
void new TeamMcpConfigBuilder().gcStaleConfigs();
void teamDataService

View file

@ -97,6 +97,7 @@ import {
} from '@shared/utils/effortLevels';
import { isTeamProviderBackendId, migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
import { isTeamProviderId } from '@shared/utils/teamProvider';
import {
buildStandaloneSlashCommandMeta,
parseStandaloneSlashCommand,
@ -1124,16 +1125,14 @@ function isValidEffort(value: unknown, providerId?: TeamProviderId | null): valu
function parseOptionalMemberProviderId(
value: unknown
):
| { valid: true; value: 'anthropic' | 'codex' | 'gemini' | undefined }
| { valid: false; error: string } {
): { valid: true; value: TeamProviderId | undefined } | { valid: false; error: string } {
if (value === undefined || value === null || value === '') {
return { valid: true, value: undefined };
}
if (value === 'anthropic' || value === 'codex' || value === 'gemini') {
if (isTeamProviderId(value)) {
return { valid: true, value };
}
return { valid: false, error: 'member providerId must be anthropic, codex, or gemini' };
return { valid: false, error: 'member providerId must be anthropic, codex, gemini, or opencode' };
}
function parseOptionalProviderBackendId(
@ -1701,7 +1700,7 @@ async function handlePrepareProvisioning(
): Promise<IpcResult<TeamProvisioningPrepareResult>> {
let validatedCwd: string | undefined;
let validatedProviderId: TeamLaunchRequest['providerId'];
let validatedProviderIds: ('anthropic' | 'codex' | 'gemini')[] | undefined;
let validatedProviderIds: TeamProviderId[] | undefined;
let validatedSelectedModels: string[] | undefined;
let validatedLimitContext: boolean | undefined;
if (cwd !== undefined) {
@ -1714,8 +1713,8 @@ async function handlePrepareProvisioning(
}
}
if (providerId !== undefined) {
if (providerId !== 'anthropic' && providerId !== 'codex' && providerId !== 'gemini') {
return { success: false, error: 'providerId must be anthropic, codex, or gemini' };
if (!isTeamProviderId(providerId)) {
return { success: false, error: 'providerId must be anthropic, codex, gemini, or opencode' };
}
validatedProviderId = providerId;
}
@ -1723,10 +1722,13 @@ async function handlePrepareProvisioning(
if (!Array.isArray(providerIds)) {
return { success: false, error: 'providerIds must be an array when provided' };
}
const normalized: ('anthropic' | 'codex' | 'gemini')[] = [];
const normalized: TeamProviderId[] = [];
for (const entry of providerIds) {
if (entry !== 'anthropic' && entry !== 'codex' && entry !== 'gemini') {
return { success: false, error: 'providerIds entries must be anthropic, codex, or gemini' };
if (!isTeamProviderId(entry)) {
return {
success: false,
error: 'providerIds entries must be anthropic, codex, gemini, or opencode',
};
}
if (!normalized.includes(entry)) {
normalized.push(entry);
@ -3283,7 +3285,7 @@ async function handleReplaceMembers(
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: EffortLevel;
}[] = [];

View file

@ -153,6 +153,7 @@ function cloneCliInstallationStatus(status: CliInstallationStatus): CliInstallat
...provider,
modelVerificationState: provider.modelVerificationState ?? 'idle',
modelCatalog: provider.modelCatalog ? structuredClone(provider.modelCatalog) : null,
detailMessage: provider.detailMessage ?? null,
modelAvailability: provider.modelAvailability?.map((item) => ({ ...item })) ?? [],
runtimeCapabilities: provider.runtimeCapabilities
? structuredClone(provider.runtimeCapabilities)
@ -763,15 +764,18 @@ export class CliInstallerService {
return null;
}
const providerStatus = await this.multimodelBridgeService.getProviderStatus(
binaryPath,
providerId
);
const nextProviderStatus = this.applyProviderModelAvailabilityToProvider(
binaryPath,
versionProbe.version,
providerStatus
);
const providerStatus =
providerId === 'opencode'
? await this.multimodelBridgeService.verifyProviderStatus(binaryPath, providerId)
: await this.multimodelBridgeService.getProviderStatus(binaryPath, providerId);
const nextProviderStatus =
providerId === 'opencode'
? await this.multimodelBridgeService.verifyOpenCodeModels(binaryPath, providerStatus)
: this.applyProviderModelAvailabilityToProvider(
binaryPath,
versionProbe.version,
providerStatus
);
this.updateLatestProviderStatus(nextProviderStatus);
if (this.latestStatusSnapshot) {
this.publishStatusSnapshot(this.latestStatusSnapshot);

View file

@ -18,6 +18,7 @@ import {
getTeamsBasePath,
getTodosBasePath,
} from '@main/utils/pathDecoder';
import { OPENCODE_TASK_LOG_ATTRIBUTION_FILE } from '@shared/constants/opencodeTaskLogAttribution';
import { createLogger } from '@shared/utils/logger';
import { EventEmitter } from 'events';
import * as fs from 'fs';
@ -1007,6 +1008,16 @@ export class FileWatcher extends EventEmitter {
detail: relative,
};
this.emit('team-change', event);
return;
}
if (relative === OPENCODE_TASK_LOG_ATTRIBUTION_FILE) {
const event: TeamChangeEvent = {
type: 'log-source-change',
teamName,
detail: relative,
};
this.emit('team-change', event);
}
}

View file

@ -1,6 +1,7 @@
import { execCli } from '@main/utils/childProcess';
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
import { createLogger } from '@shared/utils/logger';
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
import {
createDefaultCliExtensionCapabilities,
createLegacyRuntimeFallbackCliExtensionCapabilities,
@ -10,12 +11,18 @@ import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
import { providerConnectionService } from './ProviderConnectionService';
import type { CliProviderId, CliProviderReasoningEffort, CliProviderStatus } from '@shared/types';
import type {
CliProviderId,
CliProviderModelAvailability,
CliProviderReasoningEffort,
CliProviderStatus,
} from '@shared/types';
const logger = createLogger('ClaudeMultimodelBridgeService');
const PROVIDER_STATUS_TIMEOUT_MS = 10_000;
const PROVIDER_MODELS_TIMEOUT_MS = 10_000;
const OPENCODE_MODEL_VERIFY_TIMEOUT_MS = 60_000;
interface RuntimeExtensionCapabilityResponse {
status?: 'supported' | 'read-only' | 'unsupported';
@ -94,6 +101,7 @@ interface ProviderStatusCommandResponse {
verificationState?: 'verified' | 'unknown' | 'offline' | 'error';
canLoginFromUi?: boolean;
statusMessage?: string | null;
detailMessage?: string | null;
capabilities?: {
teamLaunch?: boolean;
oneShot?: boolean;
@ -179,7 +187,141 @@ interface UnifiedRuntimeStatusResponse {
>;
}
const ORDERED_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'gemini'];
interface OpenCodeRuntimeVerifyResponse {
schemaVersion?: number;
providerId?: 'opencode';
snapshot?: {
detected?: boolean;
hostHealthy?: boolean;
probeError?: string | null;
diagnostics?: string[];
host?: {
version?: string | null;
resolvedConfigFingerprint?: string | null;
} | null;
profile?: {
profileRootKey?: string;
projectBehaviorFingerprint?: string;
managedConfigFingerprint?: string;
} | null;
config?: {
default_agent?: string;
share?: string | null;
snapshot?: boolean;
autoupdate?: boolean | string;
} | null;
} | null;
}
export interface OpenCodeRuntimeTranscriptResponse {
schemaVersion?: number;
providerId?: 'opencode';
transcript?: {
sessionId?: string;
durableState?: string;
staleReason?: string | null;
messageCount?: number;
toolCallCount?: number;
errorCount?: number;
latestAssistantText?: string | null;
latestAssistantPreview?: string | null;
messages?: unknown[];
diagnostics?: string[];
logProjection?: {
sessionId?: string;
durableState?: string;
sourceMessageCount?: number;
projectedMessageCount?: number;
syntheticMessageCount?: number;
toolCallCount?: number;
errorCount?: number;
diagnostics?: string[];
messages?: OpenCodeRuntimeTranscriptLogMessage[];
} | null;
} | null;
}
export type OpenCodeRuntimeTranscriptLogContentBlock =
| {
type: 'text';
text: string;
}
| {
type: 'thinking';
thinking: string;
signature: string;
}
| {
type: 'tool_use';
id: string;
name: string;
input: Record<string, unknown>;
}
| {
type: 'tool_result';
tool_use_id: string;
content: string | OpenCodeRuntimeTranscriptLogContentBlock[];
is_error?: boolean;
};
export interface OpenCodeRuntimeTranscriptLogToolCall {
id: string;
name: string;
input: Record<string, unknown>;
isTask: boolean;
taskDescription?: string;
taskSubagentType?: string;
}
export interface OpenCodeRuntimeTranscriptLogToolResult {
toolUseId: string;
content: string | OpenCodeRuntimeTranscriptLogContentBlock[];
isError: boolean;
}
export interface OpenCodeRuntimeTranscriptLogMessage {
uuid: string;
parentUuid: string | null;
type: 'assistant' | 'user' | 'system';
timestamp: string;
role?: string;
content: OpenCodeRuntimeTranscriptLogContentBlock[] | string;
model?: string;
agentName?: string;
isMeta: boolean;
sessionId: string;
toolCalls: OpenCodeRuntimeTranscriptLogToolCall[];
toolResults: OpenCodeRuntimeTranscriptLogToolResult[];
sourceToolUseID?: string;
sourceToolAssistantUUID?: string;
subtype?: string;
level?: string;
}
interface OpenCodeRuntimeVerifyModelResponse {
schemaVersion?: number;
providerId?: 'opencode';
result?: {
modelId?: string;
outcome?: 'available' | 'unavailable' | 'unknown';
reason?: string | null;
} | null;
}
const ORDERED_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'gemini', 'opencode'];
function getProviderDisplayName(providerId: CliProviderId): string {
switch (providerId) {
case 'anthropic':
return 'Anthropic';
case 'codex':
return 'Codex';
case 'gemini':
return 'Gemini';
case 'opencode':
return 'OpenCode';
}
}
function extractJsonObject<T>(raw: string): T {
const trimmed = raw.trim();
@ -198,17 +340,17 @@ function extractJsonObject<T>(raw: string): T {
function createDefaultProviderStatus(providerId: CliProviderId): CliProviderStatus {
return {
providerId,
displayName:
providerId === 'anthropic' ? 'Anthropic' : providerId === 'codex' ? 'Codex' : 'Gemini',
displayName: getProviderDisplayName(providerId),
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
canLoginFromUi: providerId !== 'opencode',
capabilities: {
teamLaunch: false,
oneShot: false,
@ -428,6 +570,7 @@ export class ClaudeMultimodelBridgeService {
authMethod: runtimeStatus.authMethod ?? null,
verificationState: runtimeStatus.verificationState ?? 'unknown',
statusMessage: runtimeStatus.statusMessage ?? null,
detailMessage: runtimeStatus.detailMessage ?? null,
canLoginFromUi: runtimeStatus.canLoginFromUi !== false,
capabilities: {
teamLaunch: runtimeStatus.capabilities?.teamLaunch === true,
@ -514,6 +657,7 @@ export class ClaudeMultimodelBridgeService {
authMethod: null,
verificationState: 'error',
statusMessage: issue,
detailMessage: null,
backend: null,
};
}
@ -525,6 +669,94 @@ export class ClaudeMultimodelBridgeService {
return providers.map((provider) => this.applyConnectionIssue(provider, connectionIssues));
}
private async getOpenCodeVerifySnapshot(
binaryPath: string
): Promise<OpenCodeRuntimeVerifyResponse['snapshot'] | null> {
const { env } = await this.buildCliEnv(binaryPath);
const { stdout } = await execCli(
binaryPath,
['runtime', 'verify', '--json', '--provider', 'opencode'],
{
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
}
);
const parsed = extractJsonObject<OpenCodeRuntimeVerifyResponse>(stdout);
return parsed.providerId === 'opencode' ? (parsed.snapshot ?? null) : null;
}
private mergeOpenCodeVerification(
provider: CliProviderStatus,
snapshot: OpenCodeRuntimeVerifyResponse['snapshot']
): CliProviderStatus {
if (!snapshot) {
return provider;
}
const diagnostics = snapshot.diagnostics ?? [];
const diagnosticsSummary = diagnostics.slice(0, 2).join(' - ');
const liveIssuesPresent =
snapshot.detected === false ||
snapshot.hostHealthy !== true ||
Boolean(snapshot.probeError) ||
diagnostics.length > 0;
const detailParts = [
provider.detailMessage ?? null,
snapshot.host?.resolvedConfigFingerprint
? `live ${snapshot.host.resolvedConfigFingerprint.slice(0, 12)}`
: null,
snapshot.profile?.managedConfigFingerprint
? `managed ${snapshot.profile.managedConfigFingerprint.slice(0, 12)}`
: null,
snapshot.profile?.projectBehaviorFingerprint
? `behavior ${snapshot.profile.projectBehaviorFingerprint.slice(0, 12)}`
: null,
diagnosticsSummary || null,
].filter((value): value is string => Boolean(value));
const nextDiagnostics = [
...(provider.externalRuntimeDiagnostics ?? []),
{
id: 'opencode-live-host',
label: 'OpenCode live host',
detected: snapshot.hostHealthy === true,
statusMessage: snapshot.hostHealthy === true ? 'Healthy' : 'Unavailable',
detailMessage: snapshot.probeError ?? null,
},
{
id: 'opencode-managed-runtime',
label: 'OpenCode managed runtime',
detected: !liveIssuesPresent,
statusMessage: liveIssuesPresent
? 'Live verification found runtime drift'
: 'Managed runtime verified',
detailMessage: diagnosticsSummary || null,
},
];
return {
...provider,
verificationState: liveIssuesPresent ? 'error' : 'verified',
statusMessage: liveIssuesPresent
? (snapshot.probeError ??
diagnostics[0] ??
'OpenCode live verification found runtime drift')
: provider.statusMessage,
detailMessage: detailParts.length > 0 ? detailParts.join(' - ') : provider.detailMessage,
externalRuntimeDiagnostics: nextDiagnostics,
backend: provider.backend
? {
...provider.backend,
authMethodDetail:
snapshot.config?.default_agent === 'teammate'
? 'managed teammate agent'
: (provider.backend.authMethodDetail ?? null),
}
: provider.backend,
};
}
async getProviderStatus(
binaryPath: string,
providerId: CliProviderId
@ -565,6 +797,134 @@ export class ClaudeMultimodelBridgeService {
);
}
async verifyProviderStatus(
binaryPath: string,
providerId: CliProviderId
): Promise<CliProviderStatus> {
const provider = await this.getProviderStatus(binaryPath, providerId);
if (providerId !== 'opencode') {
return provider;
}
try {
const snapshot = await this.getOpenCodeVerifySnapshot(binaryPath);
return this.mergeOpenCodeVerification(provider, snapshot);
} catch (error) {
logger.warn(
`OpenCode live verification unavailable: ${
error instanceof Error ? error.message : String(error)
}`
);
return {
...provider,
verificationState: 'error',
statusMessage: 'OpenCode live verification failed',
detailMessage: error instanceof Error ? error.message : String(error),
};
}
}
async getOpenCodeTranscript(
binaryPath: string,
params: {
teamId: string;
memberName: string;
limit?: number;
}
): Promise<OpenCodeRuntimeTranscriptResponse['transcript'] | null> {
const { env } = await this.buildCliEnv(binaryPath);
const args = [
'runtime',
'transcript',
'--json',
'--provider',
'opencode',
'--team',
params.teamId,
'--member',
params.memberName,
];
if (typeof params.limit === 'number') {
args.push('--limit', String(params.limit));
}
const { stdout } = await execCli(binaryPath, args, {
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
});
const parsed = extractJsonObject<OpenCodeRuntimeTranscriptResponse>(stdout);
return parsed.providerId === 'opencode' ? (parsed.transcript ?? null) : null;
}
private async verifyOpenCodeModel(
binaryPath: string,
modelId: string
): Promise<CliProviderModelAvailability> {
const { env } = await this.buildCliEnv(binaryPath);
try {
const { stdout } = await execCli(
binaryPath,
['runtime', 'verify-model', '--json', '--provider', 'opencode', '--model', modelId],
{
timeout: OPENCODE_MODEL_VERIFY_TIMEOUT_MS,
env,
}
);
const parsed = extractJsonObject<OpenCodeRuntimeVerifyModelResponse>(stdout);
const outcome = parsed.providerId === 'opencode' ? parsed.result?.outcome : undefined;
const reason = parsed.providerId === 'opencode' ? (parsed.result?.reason ?? null) : null;
return {
modelId,
status:
outcome === 'available'
? 'available'
: outcome === 'unavailable'
? 'unavailable'
: 'unknown',
reason,
checkedAt: new Date().toISOString(),
};
} catch (error) {
return {
modelId,
status: 'unknown',
reason: error instanceof Error ? error.message : String(error),
checkedAt: new Date().toISOString(),
};
}
}
async verifyOpenCodeModels(
binaryPath: string,
provider: CliProviderStatus
): Promise<CliProviderStatus> {
const visibleModels = filterVisibleProviderRuntimeModels(provider.providerId, provider.models);
if (
provider.providerId !== 'opencode' ||
provider.supported !== true ||
provider.authenticated !== true ||
visibleModels.length === 0
) {
return {
...provider,
modelVerificationState: 'idle',
modelAvailability: [],
};
}
const modelAvailability: CliProviderModelAvailability[] = [];
for (const modelId of visibleModels) {
modelAvailability.push(await this.verifyOpenCodeModel(binaryPath, modelId));
}
return {
...provider,
modelVerificationState: 'verified',
modelAvailability,
};
}
private async buildGeminiStatus(binaryPath: string): Promise<CliProviderStatus> {
const provider = createDefaultProviderStatus('gemini');
const { env } = await this.buildProviderCliEnv(binaryPath, 'gemini');
@ -686,6 +1046,7 @@ export class ClaudeMultimodelBridgeService {
authMethod: runtimeStatus.authMethod ?? null,
verificationState: runtimeStatus.verificationState ?? 'unknown',
statusMessage: runtimeStatus.statusMessage ?? null,
detailMessage: runtimeStatus.detailMessage ?? null,
canLoginFromUi: runtimeStatus.canLoginFromUi !== false,
capabilities: {
teamLaunch: runtimeStatus.capabilities?.teamLaunch === true,

View file

@ -44,6 +44,11 @@ const PROVIDER_CAPABILITIES: Record<
supportsApiKey: true,
configurableAuthModes: [],
},
opencode: {
supportsOAuth: false,
supportsApiKey: false,
configurableAuthModes: [],
},
};
const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
@ -184,7 +189,7 @@ export class ProviderConnectionService {
async applyAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini'] as const) {
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId);
}
return nextEnv;
@ -238,7 +243,7 @@ export class ProviderConnectionService {
async augmentAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
let nextEnv = env;
for (const providerId of ['anthropic', 'codex', 'gemini'] as const) {
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId);
}
return nextEnv;
@ -308,7 +313,7 @@ export class ProviderConnectionService {
async getConfiguredConnectionIssues(
env: NodeJS.ProcessEnv,
providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini'],
providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini', 'opencode'],
runtimeBackendOverrides?: Partial<Record<CliProviderId, string>>
): Promise<Partial<Record<CliProviderId, string>>> {
const issues: Partial<Record<CliProviderId, string>> = {};

View file

@ -6,7 +6,7 @@ import { configManager } from '../infrastructure/ConfigManager';
import {
applyConfiguredRuntimeBackendsEnv,
applyProviderRuntimeEnv,
resolveTeamProviderId,
resolveRuntimeProviderId,
} from './providerRuntimeEnv';
import type { CliProviderId, TeamProviderId } from '@shared/types';
@ -68,19 +68,19 @@ export function buildRuntimeBaseEnv(options: BuildRuntimeBaseEnvOptions = {}): {
};
}
const resolvedProviderId = resolveTeamProviderId(options.providerId);
const runtimeProviderId = resolveRuntimeProviderId(options.providerId);
applyProviderRuntimeEnv(env, options.providerId);
if (resolvedProviderId === 'codex' && options.providerBackendId?.trim()) {
if (runtimeProviderId === 'codex' && options.providerBackendId?.trim()) {
env.CLAUDE_CODE_CODEX_BACKEND = options.providerBackendId.trim();
}
if (resolvedProviderId === 'gemini' && options.providerBackendId?.trim()) {
if (runtimeProviderId === 'gemini' && options.providerBackendId?.trim()) {
env.CLAUDE_CODE_GEMINI_BACKEND = options.providerBackendId.trim();
}
return {
env,
resolvedProviderId,
resolvedProviderId: runtimeProviderId,
};
}

View file

@ -1,6 +1,8 @@
import { ConfigManager } from '../infrastructure/ConfigManager';
import type { TeamProviderId } from '@shared/types';
import type { CliProviderId, TeamProviderId } from '@shared/types';
type RuntimeEnvProviderId = CliProviderId | TeamProviderId;
const PROVIDER_ROUTING_ENV_KEYS = [
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
@ -32,10 +34,9 @@ export function applyConfiguredRuntimeBackendsEnv(
export function applyProviderRuntimeEnv(
env: NodeJS.ProcessEnv,
providerId: TeamProviderId | undefined
providerId: RuntimeEnvProviderId | undefined
): NodeJS.ProcessEnv {
const resolvedProvider: TeamProviderId =
providerId === 'codex' || providerId === 'gemini' ? providerId : 'anthropic';
const resolvedProvider = resolveRuntimeProviderId(providerId);
for (const key of PROVIDER_ROUTING_ENV_KEYS) {
env[key] = undefined;
@ -52,6 +53,18 @@ export function applyProviderRuntimeEnv(
return env;
}
export function resolveTeamProviderId(providerId: TeamProviderId | undefined): TeamProviderId {
return providerId === 'codex' || providerId === 'gemini' ? providerId : 'anthropic';
export function resolveRuntimeProviderId(
providerId: RuntimeEnvProviderId | undefined
): CliProviderId {
if (providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode') {
return providerId;
}
return 'anthropic';
}
export function resolveTeamProviderId(providerId: TeamProviderId | undefined): TeamProviderId {
return providerId === 'codex' || providerId === 'gemini' || providerId === 'opencode'
? providerId
: 'anthropic';
}

View file

@ -78,6 +78,7 @@ import type {
TeamMember,
TeamMemberActivityMeta,
TeamProcess,
TeamProviderId,
TeamSummary,
TeamTask,
TeamTaskStatus,
@ -1315,7 +1316,7 @@ export class TeamDataService {
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: TeamMember['effort'];
}[];

View file

@ -7,7 +7,7 @@ import * as path from 'path';
import { atomicWriteAsync } from './atomicWrite';
interface McpLaunchSpec {
export interface McpLaunchSpec {
command: string;
args: string[];
}
@ -202,7 +202,7 @@ async function resolvePackagedServerEntry(): Promise<string> {
}
}
async function resolveMcpLaunchSpec(): Promise<McpLaunchSpec> {
export async function resolveAgentTeamsMcpLaunchSpec(): Promise<McpLaunchSpec> {
const checked: string[] = [];
// 1. Packaged Electron app — prefer stable copy, fall back to resourcesPath
@ -250,7 +250,7 @@ async function resolveMcpLaunchSpec(): Promise<McpLaunchSpec> {
export class TeamMcpConfigBuilder {
async writeConfigFile(_projectPath?: string): Promise<string> {
const launchSpec = await resolveMcpLaunchSpec();
const launchSpec = await resolveAgentTeamsMcpLaunchSpec();
const configDir = getMcpConfigsBasePath();
const configPath = path.join(
configDir,

View file

@ -5,8 +5,15 @@ import {
} from '@shared/utils/teamMemberName';
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import type { TeamConfig, TeamMember, TeamMemberSnapshot, TeamTaskWithKanban } from '@shared/types';
import type {
TeamConfig,
TeamMember,
TeamMemberSnapshot,
TeamProviderId,
TeamTaskWithKanban,
} from '@shared/types';
const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
const CROSS_TEAM_TOOL_RECIPIENT_NAMES = new Set([
@ -121,7 +128,7 @@ export class TeamMemberResolver {
agentType?: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: TeamMember['effort'];
color?: string;
@ -131,17 +138,10 @@ export class TeamMemberResolver {
if (Array.isArray(config.members)) {
for (const m of config.members) {
if (typeof m?.name === 'string' && m.name.trim() !== '') {
const configMember = m as TeamMember & { provider?: 'anthropic' | 'codex' | 'gemini' };
const configMember = m as TeamMember & { provider?: TeamProviderId };
const providerId =
configMember.providerId === 'anthropic' ||
configMember.providerId === 'codex' ||
configMember.providerId === 'gemini'
? configMember.providerId
: configMember.provider === 'anthropic' ||
configMember.provider === 'codex' ||
configMember.provider === 'gemini'
? configMember.provider
: undefined;
normalizeOptionalTeamProviderId(configMember.providerId) ??
normalizeOptionalTeamProviderId(configMember.provider);
configMemberMap.set(m.name.trim(), {
agentId: configMember.agentId,
agentType: configMember.agentType,
@ -164,7 +164,7 @@ export class TeamMemberResolver {
agentType?: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: TeamMember['effort'];
color?: string;

View file

@ -21,7 +21,7 @@ export interface TeamMetaFile {
color?: string;
cwd: string;
prompt?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
providerBackendId?: string;
model?: string;
effort?: string;

File diff suppressed because it is too large Load diff

View file

@ -16,12 +16,67 @@ export { HunkSnippetMatcher } from './HunkSnippetMatcher';
export { MemberStatsComputer } from './MemberStatsComputer';
export { ReviewApplierService } from './ReviewApplierService';
export { TaskBoundaryParser } from './TaskBoundaryParser';
export {
isTeamRuntimeProviderId,
OpenCodeTeamRuntimeAdapter,
TeamRuntimeAdapterRegistry,
TEAM_RUNTIME_PROVIDER_IDS,
} from './runtime';
export { OpenCodeReadinessBridge } from './opencode/bridge/OpenCodeReadinessBridge';
export type {
OpenCodeTeamLaunchMode,
OpenCodeTeamRuntimeAdapterOptions,
OpenCodeTeamRuntimeBridgePort,
TeamLaunchRuntimeAdapter,
TeamRuntimeLaunchInput,
TeamRuntimeLaunchResult,
TeamRuntimeMemberLaunchEvidence,
TeamRuntimeMemberSpec,
TeamRuntimeMemberStopEvidence,
TeamRuntimePrepareFailure,
TeamRuntimePrepareResult,
TeamRuntimePrepareSuccess,
TeamRuntimeProviderId,
TeamRuntimeReconcileInput,
TeamRuntimeReconcileReason,
TeamRuntimeReconcileResult,
TeamRuntimeStopInput,
TeamRuntimeStopReason,
TeamRuntimeStopResult,
} from './runtime';
export type {
OpenCodeReadinessBridgeCommandBody,
OpenCodeReadinessBridgeCommandExecutor,
OpenCodeReadinessBridgeOptions,
} from './opencode/bridge/OpenCodeReadinessBridge';
export { BoardTaskActivityDetailService } from './taskLogs/activity/BoardTaskActivityDetailService';
export { BoardTaskActivityRecordSource } from './taskLogs/activity/BoardTaskActivityRecordSource';
export { BoardTaskActivityService } from './taskLogs/activity/BoardTaskActivityService';
export { BoardTaskExactLogDetailService } from './taskLogs/exact/BoardTaskExactLogDetailService';
export { BoardTaskExactLogsService } from './taskLogs/exact/BoardTaskExactLogsService';
export { BoardTaskLogStreamService } from './taskLogs/stream/BoardTaskLogStreamService';
export { OpenCodeTaskLogAttributionService } from './taskLogs/stream/OpenCodeTaskLogAttributionService';
export type {
OpenCodeTaskLogAttributionBulkWriteOutcome,
OpenCodeTaskLogAttributionMemberWindowInput,
OpenCodeTaskLogAttributionRecordDraft,
OpenCodeTaskLogAttributionRecordWriteOutcome,
OpenCodeTaskLogAttributionReplaceInput,
OpenCodeTaskLogAttributionTaskInput,
OpenCodeTaskLogAttributionTaskSessionInput,
OpenCodeTaskLogAttributionWriter,
} from './taskLogs/stream/OpenCodeTaskLogAttributionService';
export {
OpenCodeTaskLogAttributionStore,
getOpenCodeTaskLogAttributionPath,
} from './taskLogs/stream/OpenCodeTaskLogAttributionStore';
export type {
OpenCodeTaskLogAttributionReader,
OpenCodeTaskLogAttributionRecord,
OpenCodeTaskLogAttributionScope,
OpenCodeTaskLogAttributionSource,
OpenCodeTaskLogAttributionWriteResult,
} from './taskLogs/stream/OpenCodeTaskLogAttributionStore';
export { TeamAttachmentStore } from './TeamAttachmentStore';
export { TeamBackupService } from './TeamBackupService';
export { TeamConfigReader } from './TeamConfigReader';

View file

@ -1,8 +1,10 @@
import type { TeamProviderId } from '@shared/types';
export interface MemberDiffInput {
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
removedAt?: number | string | null;
}
@ -12,7 +14,7 @@ export interface ReplaceMembersDiff {
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
}[];
removed: string[];
@ -65,7 +67,7 @@ export function buildReplaceMembersDiff(
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
}[]
): ReplaceMembersDiff {

View file

@ -0,0 +1,267 @@
import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import * as path from 'path';
import { execCli } from '@main/utils/childProcess';
import {
extractRunId,
OPEN_CODE_BRIDGE_SCHEMA_VERSION,
parseSingleBridgeJsonResult,
validateBridgeResultEnvelope,
type OpenCodeBridgeCommandEnvelope,
type OpenCodeBridgeCommandName,
type OpenCodeBridgeDiagnosticEvent,
type OpenCodeBridgeFailure,
type OpenCodeBridgeFailureKind,
type OpenCodeBridgeResult,
} from './OpenCodeBridgeCommandContract';
export interface OpenCodeBridgeProcessRunInput {
binaryPath: string;
args: string[];
cwd: string;
timeoutMs: number;
stdoutLimitBytes: number;
stderrLimitBytes: number;
env: NodeJS.ProcessEnv;
}
export interface OpenCodeBridgeProcessRunResult {
stdout: string;
stderr: string;
exitCode: number | null;
timedOut: boolean;
}
export interface OpenCodeBridgeProcessRunner {
run(input: OpenCodeBridgeProcessRunInput): Promise<OpenCodeBridgeProcessRunResult>;
}
export interface OpenCodeBridgeDiagnosticsSink {
append(event: OpenCodeBridgeDiagnosticEvent): Promise<void>;
}
export interface OpenCodeBridgeCommandClientOptions {
binaryPath: string;
tempDirectory: string;
processRunner?: OpenCodeBridgeProcessRunner;
diagnostics?: OpenCodeBridgeDiagnosticsSink;
requestIdFactory?: () => string;
diagnosticIdFactory?: () => string;
clock?: () => Date;
env?: NodeJS.ProcessEnv;
keepInputFile?: boolean;
}
const DEFAULT_STDOUT_LIMIT_BYTES = 1_000_000;
const DEFAULT_STDERR_LIMIT_BYTES = 256_000;
export class ExecCliOpenCodeBridgeProcessRunner implements OpenCodeBridgeProcessRunner {
async run(input: OpenCodeBridgeProcessRunInput): Promise<OpenCodeBridgeProcessRunResult> {
try {
const result = await execCli(input.binaryPath, input.args, {
cwd: input.cwd,
timeout: input.timeoutMs,
maxBuffer: input.stdoutLimitBytes + input.stderrLimitBytes,
env: input.env,
});
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: 0,
timedOut: false,
};
} catch (error) {
const failure = error as NodeJS.ErrnoException & {
stdout?: string | Buffer;
stderr?: string | Buffer;
killed?: boolean;
signal?: string;
};
const message = failure.message ?? '';
return {
stdout: bufferToString(failure.stdout),
stderr: bufferToString(failure.stderr) || message,
exitCode: typeof failure.code === 'number' ? failure.code : null,
timedOut:
failure.killed === true ||
failure.signal === 'SIGTERM' ||
/timed out|timeout/i.test(message),
};
}
}
}
export class OpenCodeBridgeCommandClient {
private readonly binaryPath: string;
private readonly tempDirectory: string;
private readonly processRunner: OpenCodeBridgeProcessRunner;
private readonly diagnostics: OpenCodeBridgeDiagnosticsSink | null;
private readonly requestIdFactory: () => string;
private readonly diagnosticIdFactory: () => string;
private readonly clock: () => Date;
private readonly env: NodeJS.ProcessEnv;
private readonly keepInputFile: boolean;
constructor(options: OpenCodeBridgeCommandClientOptions) {
this.binaryPath = options.binaryPath;
this.tempDirectory = options.tempDirectory;
this.processRunner = options.processRunner ?? new ExecCliOpenCodeBridgeProcessRunner();
this.diagnostics = options.diagnostics ?? null;
this.requestIdFactory = options.requestIdFactory ?? (() => `opencode-bridge-${randomUUID()}`);
this.diagnosticIdFactory =
options.diagnosticIdFactory ?? (() => `opencode-bridge-diagnostic-${randomUUID()}`);
this.clock = options.clock ?? (() => new Date());
this.env = options.env ?? process.env;
this.keepInputFile = options.keepInputFile ?? false;
}
async execute<TBody, TData>(
command: OpenCodeBridgeCommandName,
body: TBody,
options: {
cwd: string;
timeoutMs: number;
requestId?: string;
stdoutLimitBytes?: number;
stderrLimitBytes?: number;
}
): Promise<OpenCodeBridgeResult<TData>> {
const envelope: OpenCodeBridgeCommandEnvelope<TBody> = {
schemaVersion: OPEN_CODE_BRIDGE_SCHEMA_VERSION,
requestId: options.requestId ?? this.requestIdFactory(),
command,
cwd: options.cwd,
startedAt: this.clock().toISOString(),
timeoutMs: options.timeoutMs,
body,
};
const inputPath = await this.writeInputFile(envelope);
try {
const processResult = await this.processRunner.run({
binaryPath: this.binaryPath,
args: ['runtime', 'opencode-command', '--json', '--input', inputPath],
cwd: options.cwd,
timeoutMs: options.timeoutMs,
stdoutLimitBytes: options.stdoutLimitBytes ?? DEFAULT_STDOUT_LIMIT_BYTES,
stderrLimitBytes: options.stderrLimitBytes ?? DEFAULT_STDERR_LIMIT_BYTES,
env: this.env,
});
if (processResult.timedOut) {
return this.contractFailure(
envelope,
'timeout',
'OpenCode bridge command timed out',
true,
{
stderr: redactBridgeDiagnosticText(processResult.stderr),
}
);
}
if (processResult.exitCode !== 0) {
return this.contractFailure(
envelope,
'provider_error',
'OpenCode bridge command failed',
true,
{
exitCode: processResult.exitCode,
stderr: redactBridgeDiagnosticText(processResult.stderr),
}
);
}
const parsed = parseSingleBridgeJsonResult<TData>(processResult.stdout);
if (!parsed.ok) {
return this.contractFailure(envelope, 'contract_violation', parsed.error, false, {
stdoutPreview: redactBridgeDiagnosticText(processResult.stdout.slice(0, 2_000)),
});
}
const validation = validateBridgeResultEnvelope(parsed.value, envelope);
if (!validation.ok) {
return this.contractFailure(envelope, 'contract_violation', validation.reason, false, {});
}
return parsed.value;
} finally {
if (!this.keepInputFile) {
await fs.unlink(inputPath).catch(() => undefined);
}
}
}
private async writeInputFile<TBody>(
envelope: OpenCodeBridgeCommandEnvelope<TBody>
): Promise<string> {
await fs.mkdir(this.tempDirectory, { recursive: true, mode: 0o700 });
const inputPath = path.join(this.tempDirectory, `opencode-command-${envelope.requestId}.json`);
await fs.writeFile(inputPath, `${JSON.stringify(envelope, null, 2)}\n`, {
encoding: 'utf8',
mode: 0o600,
});
return inputPath;
}
private async contractFailure<TBody>(
envelope: OpenCodeBridgeCommandEnvelope<TBody>,
kind: OpenCodeBridgeFailureKind,
message: string,
retryable: boolean,
details: Record<string, unknown>
): Promise<OpenCodeBridgeFailure> {
const completedAt = this.clock().toISOString();
const diagnostic: OpenCodeBridgeDiagnosticEvent = {
id: this.diagnosticIdFactory(),
type:
kind === 'timeout'
? 'opencode_bridge_unknown_outcome'
: 'opencode_bridge_contract_violation',
providerId: 'opencode',
runId: extractRunId(envelope.body) ?? undefined,
severity: retryable ? 'warning' : 'error',
message,
data: details,
createdAt: completedAt,
};
await this.diagnostics?.append(diagnostic);
return {
ok: false,
schemaVersion: OPEN_CODE_BRIDGE_SCHEMA_VERSION,
requestId: envelope.requestId,
command: envelope.command,
completedAt,
durationMs: Math.max(0, Date.parse(completedAt) - Date.parse(envelope.startedAt)),
error: {
kind,
message,
retryable,
details,
},
diagnostics: [diagnostic],
};
}
}
export function redactBridgeDiagnosticText(value: string): string {
const capped = value.length > 4_000 ? `${value.slice(0, 4_000)}...[truncated]` : value;
return capped
.replace(/(authorization:\s*bearer\s+)[^\s]+/gi, '$1[redacted]')
.replace(/((?:api[_-]?key|token|password|secret)\s*[=:]\s*)[^\s"'`]+/gi, '$1[redacted]');
}
function bufferToString(value: string | Buffer | undefined): string {
if (typeof value === 'string') {
return value;
}
if (Buffer.isBuffer(value)) {
return value.toString('utf8');
}
return '';
}

View file

@ -0,0 +1,813 @@
import { createHash } from 'crypto';
export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const;
export type OpenCodeBridgeCommandName =
| 'opencode.handshake'
| 'opencode.commandStatus'
| 'opencode.readiness'
| 'opencode.launchTeam'
| 'opencode.reconcileTeam'
| 'opencode.stopTeam'
| 'opencode.answerPermission'
| 'opencode.listRuntimePermissions'
| 'opencode.getRuntimeTranscript'
| 'opencode.recoverDeliveryJournal';
export type OpenCodeTeamLaunchMode = 'disabled' | 'dogfood' | 'production';
export type OpenCodeTeamLaunchBridgeState =
| 'blocked'
| 'launching'
| 'ready'
| 'permission_blocked'
| 'failed';
export type OpenCodeTeamMemberLaunchBridgeState =
| 'created'
| 'confirmed_alive'
| 'permission_blocked'
| 'failed';
export interface OpenCodeTeamBridgeDiagnostic {
code: string;
severity: 'info' | 'warning' | 'error';
message: string;
}
export interface OpenCodeTeamBridgeWarning {
code: string;
message: string;
}
export interface OpenCodeTeamLaunchMemberCommandSpec {
name: string;
role: string;
prompt: string;
}
export interface OpenCodeLaunchTeamCommandBody {
mode: OpenCodeTeamLaunchMode;
runId: string;
teamId: string;
teamName: string;
projectPath: string;
selectedModel: string;
members: OpenCodeTeamLaunchMemberCommandSpec[];
leadPrompt: string;
expectedCapabilitySnapshotId: string | null;
manifestHighWatermark: number | null;
}
export interface OpenCodeTeamMemberLaunchCommandData {
sessionId: string;
launchState: OpenCodeTeamMemberLaunchBridgeState;
model: string;
evidence: Array<{ kind: string; observedAt: string }>;
}
export interface OpenCodeLaunchTeamCommandData {
runId: string;
teamLaunchState: OpenCodeTeamLaunchBridgeState;
members: Record<string, OpenCodeTeamMemberLaunchCommandData>;
warnings: OpenCodeTeamBridgeWarning[];
diagnostics: OpenCodeTeamBridgeDiagnostic[];
idempotencyKey?: string;
manifestHighWatermark?: number | null;
runtimeStoreManifestHighWatermark?: number | null;
durableCheckpoints?: Array<{ name: string; memberName?: string | null; observedAt: string }>;
}
export interface OpenCodeReconcileTeamCommandBody {
runId: string;
teamId: string;
teamName: string;
projectPath?: string;
expectedCapabilitySnapshotId?: string | null;
manifestHighWatermark?: number | null;
reconcileAttemptId?: string;
expectedMembers: Array<{ name: string; model: string | null }>;
reason: string;
}
export interface OpenCodeStopTeamCommandBody {
runId: string;
teamId: string;
teamName: string;
projectPath?: string;
expectedCapabilitySnapshotId?: string | null;
manifestHighWatermark?: number | null;
reason: string;
force?: boolean;
}
export interface OpenCodeStopTeamCommandData {
runId: string;
stopped: boolean;
members: Record<string, { sessionId?: string; stopped: boolean; diagnostics: string[] }>;
warnings: OpenCodeTeamBridgeWarning[];
diagnostics: OpenCodeTeamBridgeDiagnostic[];
idempotencyKey?: string;
manifestHighWatermark?: number | null;
runtimeStoreManifestHighWatermark?: number | null;
}
export type OpenCodeBridgePeerName = 'claude_team' | 'agent_teams_orchestrator';
export type OpenCodeBridgeFailureKind =
| 'unsupported_schema'
| 'unsupported_command'
| 'invalid_input'
| 'runtime_not_ready'
| 'provider_error'
| 'timeout'
| 'contract_violation'
| 'internal_error';
export interface OpenCodeBridgeDiagnosticEvent {
id?: string;
type: string;
providerId: 'opencode';
teamName?: string;
runId?: string;
severity: 'info' | 'warning' | 'error';
message: string;
data?: Record<string, unknown>;
createdAt: string;
}
export interface OpenCodeBridgeCommandEnvelope<TBody> {
schemaVersion: typeof OPEN_CODE_BRIDGE_SCHEMA_VERSION;
requestId: string;
command: OpenCodeBridgeCommandName;
cwd: string;
startedAt: string;
timeoutMs: number;
body: TBody;
}
export interface OpenCodeBridgeRuntimeSnapshot {
providerId: 'opencode';
binaryPath: string | null;
binaryFingerprint: string | null;
version: string | null;
capabilitySnapshotId: string | null;
}
export interface OpenCodeBridgeSuccess<TData> {
ok: true;
schemaVersion: typeof OPEN_CODE_BRIDGE_SCHEMA_VERSION;
requestId: string;
command: OpenCodeBridgeCommandName;
completedAt: string;
durationMs: number;
runtime: OpenCodeBridgeRuntimeSnapshot;
diagnostics: OpenCodeBridgeDiagnosticEvent[];
data: TData;
}
export interface OpenCodeBridgeFailure {
ok: false;
schemaVersion: typeof OPEN_CODE_BRIDGE_SCHEMA_VERSION;
requestId: string;
command: OpenCodeBridgeCommandName;
completedAt: string;
durationMs: number;
error: {
kind: OpenCodeBridgeFailureKind;
message: string;
retryable: boolean;
details?: Record<string, unknown>;
};
diagnostics: OpenCodeBridgeDiagnosticEvent[];
}
export type OpenCodeBridgeResult<TData> = OpenCodeBridgeSuccess<TData> | OpenCodeBridgeFailure;
export interface OpenCodeBridgePeerIdentity {
schemaVersion: typeof OPEN_CODE_BRIDGE_SCHEMA_VERSION;
peer: OpenCodeBridgePeerName;
appVersion: string;
gitSha: string | null;
buildId: string | null;
bridgeProtocol: {
minVersion: number;
currentVersion: number;
supportedCommands: OpenCodeBridgeCommandName[];
};
runtime: {
providerId: 'opencode';
binaryPath: string | null;
binaryFingerprint: string | null;
version: string | null;
capabilitySnapshotId: string | null;
runtimeStoreManifestHighWatermark: number | null;
activeRunId: string | null;
};
featureFlags: {
opencodeTeamLaunch: boolean;
opencodeStateChangingCommands: boolean;
};
}
export interface OpenCodeBridgeHandshake {
schemaVersion: typeof OPEN_CODE_BRIDGE_SCHEMA_VERSION;
requestId: string;
client: OpenCodeBridgePeerIdentity;
server: OpenCodeBridgePeerIdentity;
agreedProtocolVersion: number;
acceptedCommands: OpenCodeBridgeCommandName[];
serverTime: string;
identityHash: string;
}
export interface OpenCodeBridgeCommandPreconditions {
handshakeIdentityHash: string;
expectedRunId: string | null;
expectedCapabilitySnapshotId: string | null;
expectedBehaviorFingerprint: string | null;
expectedManifestHighWatermark: number | null;
commandLeaseId: string | null;
idempotencyKey: string;
}
export interface OpenCodeStateChangingBridgeEnvelope<
TBody,
> extends OpenCodeBridgeCommandEnvelope<TBody> {
stateChanging: true;
preconditions: OpenCodeBridgeCommandPreconditions;
}
export interface RuntimeStoreManifestEvidence {
highWatermark: number;
activeRunId?: string | null;
capabilitySnapshotId?: string | null;
}
const VALID_COMMANDS: ReadonlySet<OpenCodeBridgeCommandName> = new Set([
'opencode.handshake',
'opencode.commandStatus',
'opencode.readiness',
'opencode.launchTeam',
'opencode.reconcileTeam',
'opencode.stopTeam',
'opencode.answerPermission',
'opencode.listRuntimePermissions',
'opencode.getRuntimeTranscript',
'opencode.recoverDeliveryJournal',
]);
const VALID_FAILURE_KINDS: ReadonlySet<OpenCodeBridgeFailureKind> = new Set([
'unsupported_schema',
'unsupported_command',
'invalid_input',
'runtime_not_ready',
'provider_error',
'timeout',
'contract_violation',
'internal_error',
]);
export function isOpenCodeBridgeCommandName(value: unknown): value is OpenCodeBridgeCommandName {
return typeof value === 'string' && VALID_COMMANDS.has(value as OpenCodeBridgeCommandName);
}
export function parseSingleBridgeJsonResult<TData>(
stdout: string
): { ok: true; value: OpenCodeBridgeResult<TData> } | { ok: false; error: string } {
const trimmed = stdout.trim();
if (!trimmed) {
return { ok: false, error: 'Bridge stdout was empty' };
}
const lines = trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0);
if (lines.length !== 1) {
return {
ok: false,
error: `Bridge stdout must contain exactly one JSON line, got ${lines.length}`,
};
}
let parsed: unknown;
try {
parsed = JSON.parse(lines[0]);
} catch (error) {
return { ok: false, error: `Bridge stdout JSON parse failed: ${stringifyError(error)}` };
}
const validation = validateOpenCodeBridgeResultShape(parsed);
if (!validation.ok) {
return { ok: false, error: validation.reason };
}
return { ok: true, value: validation.value as OpenCodeBridgeResult<TData> };
}
export function validateBridgeResultEnvelope<TBody, TData>(
result: OpenCodeBridgeResult<TData>,
envelope: Pick<OpenCodeBridgeCommandEnvelope<TBody>, 'schemaVersion' | 'requestId' | 'command'>
): { ok: true } | { ok: false; reason: string } {
const shape = validateOpenCodeBridgeResultShape(result);
if (!shape.ok) {
return { ok: false, reason: shape.reason };
}
if (result.schemaVersion !== envelope.schemaVersion) {
return { ok: false, reason: 'OpenCode bridge schemaVersion mismatch' };
}
if (result.requestId !== envelope.requestId) {
return { ok: false, reason: 'OpenCode bridge requestId mismatch' };
}
if (result.command !== envelope.command) {
return { ok: false, reason: 'OpenCode bridge command mismatch' };
}
return { ok: true };
}
export function assertBridgeResultCanMutateState<TData>(
result: OpenCodeBridgeResult<TData>,
expected: {
requestId: string;
command: OpenCodeBridgeCommandName;
runId: string | null;
capabilitySnapshotId: string | null;
}
): asserts result is OpenCodeBridgeSuccess<TData> {
if (!result.ok) {
throw new Error(
`OpenCode bridge command failed: ${result.error.kind}: ${result.error.message}`
);
}
if (result.requestId !== expected.requestId) {
throw new Error('OpenCode bridge requestId mismatch');
}
if (result.command !== expected.command) {
throw new Error('OpenCode bridge command mismatch');
}
if (extractRunId(result.data) !== expected.runId) {
throw new Error('OpenCode bridge runId mismatch');
}
if (
expected.capabilitySnapshotId !== null &&
result.runtime.capabilitySnapshotId !== expected.capabilitySnapshotId
) {
throw new Error('OpenCode bridge capability snapshot mismatch');
}
}
export function validateOpenCodeBridgeHandshake(input: {
handshake: OpenCodeBridgeHandshake;
expectedClient: OpenCodeBridgePeerIdentity;
requiredCommand: OpenCodeBridgeCommandName;
expectedCapabilitySnapshotId: string | null;
expectedManifestHighWatermark: number | null;
expectedRunId: string | null;
}): { ok: true } | { ok: false; reason: string } {
const shape = validateOpenCodeBridgeHandshakeShape(input.handshake);
if (!shape.ok) {
return shape;
}
if (input.handshake.client.peer !== input.expectedClient.peer) {
return { ok: false, reason: 'Bridge handshake client peer mismatch' };
}
if (stableHash(input.handshake.client) !== stableHash(input.expectedClient)) {
return { ok: false, reason: 'Bridge handshake client identity mismatch' };
}
const minimumProtocol = Math.max(
input.handshake.client.bridgeProtocol.minVersion,
input.handshake.server.bridgeProtocol.minVersion
);
const maximumProtocol = Math.min(
input.handshake.client.bridgeProtocol.currentVersion,
input.handshake.server.bridgeProtocol.currentVersion
);
if (
input.handshake.agreedProtocolVersion < minimumProtocol ||
input.handshake.agreedProtocolVersion > maximumProtocol
) {
return { ok: false, reason: 'Bridge handshake protocol version mismatch' };
}
if (!input.handshake.acceptedCommands.includes(input.requiredCommand)) {
return { ok: false, reason: `Bridge server does not accept command ${input.requiredCommand}` };
}
if (!input.handshake.server.bridgeProtocol.supportedCommands.includes(input.requiredCommand)) {
return { ok: false, reason: `Bridge server does not support command ${input.requiredCommand}` };
}
if (
input.expectedCapabilitySnapshotId &&
input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId
) {
return { ok: false, reason: 'Bridge server capability snapshot mismatch' };
}
if (
input.expectedRunId &&
input.handshake.server.runtime.activeRunId &&
input.handshake.server.runtime.activeRunId !== input.expectedRunId
) {
return { ok: false, reason: 'Bridge server active run mismatch' };
}
const serverHighWatermark = input.handshake.server.runtime.runtimeStoreManifestHighWatermark;
if (
input.expectedManifestHighWatermark !== null &&
serverHighWatermark !== null &&
serverHighWatermark < input.expectedManifestHighWatermark
) {
return { ok: false, reason: 'Bridge server runtime manifest high watermark is stale' };
}
const expectedIdentityHash = createOpenCodeBridgeHandshakeIdentityHash(input.handshake);
if (input.handshake.identityHash !== expectedIdentityHash) {
return { ok: false, reason: 'Bridge handshake identity hash mismatch' };
}
return { ok: true };
}
export function createOpenCodeBridgeHandshakeIdentityHash(
handshake: Omit<OpenCodeBridgeHandshake, 'identityHash'> | OpenCodeBridgeHandshake
): string {
const { identityHash: _ignored, ...hashable } = handshake as OpenCodeBridgeHandshake;
return stableHash(hashable);
}
export function assertBridgeEvidenceCanCommitToRuntimeStores(input: {
result: OpenCodeBridgeResult<unknown>;
requestId: string;
command: OpenCodeBridgeCommandName;
runId: string | null;
capabilitySnapshotId: string | null;
manifest: RuntimeStoreManifestEvidence;
idempotencyKey: string;
}): asserts input is {
result: OpenCodeBridgeSuccess<unknown>;
requestId: string;
command: OpenCodeBridgeCommandName;
runId: string | null;
capabilitySnapshotId: string | null;
manifest: RuntimeStoreManifestEvidence;
idempotencyKey: string;
} {
assertBridgeResultCanMutateState(input.result, {
requestId: input.requestId,
command: input.command,
runId: input.runId,
capabilitySnapshotId: input.capabilitySnapshotId,
});
const resultManifestHighWatermark = extractManifestHighWatermark(input.result.data);
if (
typeof resultManifestHighWatermark === 'number' &&
resultManifestHighWatermark < input.manifest.highWatermark
) {
throw new Error('Bridge result manifest high watermark is stale');
}
if (extractIdempotencyKey(input.result.data) !== input.idempotencyKey) {
throw new Error('Bridge result idempotency key mismatch');
}
}
export function createOpenCodeBridgeIdempotencyKey(input: {
command: OpenCodeBridgeCommandName;
teamName: string;
runId: string | null;
body: unknown;
}): string {
const scope = [
'opencode',
sanitizeKeyPart(input.command),
sanitizeKeyPart(input.teamName),
sanitizeKeyPart(input.runId ?? 'no-run'),
].join(':');
return `${scope}:${stableHash(input).slice(0, 32)}`;
}
export function stableHash(value: unknown): string {
return createHash('sha256').update(stableJsonStringify(value)).digest('hex');
}
export function stableJsonStringify(value: unknown): string {
return JSON.stringify(normalizeStableJson(value));
}
export function extractRunId(value: unknown): string | null {
return (
extractStringByPath(value, ['runId']) ??
extractStringByPath(value, ['runtimeRunId']) ??
extractStringByPath(value, ['runtime', 'runId']) ??
extractStringByPath(value, ['launch', 'runId'])
);
}
export function extractIdempotencyKey(value: unknown): string | null {
return (
extractStringByPath(value, ['idempotencyKey']) ??
extractStringByPath(value, ['preconditions', 'idempotencyKey']) ??
extractStringByPath(value, ['command', 'idempotencyKey'])
);
}
export function extractManifestHighWatermark(value: unknown): number | null {
return (
extractNumberByPath(value, ['runtimeStoreManifestHighWatermark']) ??
extractNumberByPath(value, ['manifestHighWatermark']) ??
extractNumberByPath(value, ['manifest', 'highWatermark'])
);
}
function validateOpenCodeBridgeResultShape(
value: unknown
): { ok: true; value: OpenCodeBridgeResult<unknown> } | { ok: false; reason: string } {
if (!isRecord(value)) {
return { ok: false, reason: 'Bridge result must be a JSON object' };
}
if (value.schemaVersion !== OPEN_CODE_BRIDGE_SCHEMA_VERSION) {
return { ok: false, reason: 'Bridge result has unsupported schemaVersion' };
}
if (typeof value.ok !== 'boolean') {
return { ok: false, reason: 'Bridge result missing ok boolean' };
}
if (typeof value.requestId !== 'string' || !value.requestId.trim()) {
return { ok: false, reason: 'Bridge result missing requestId' };
}
if (!isOpenCodeBridgeCommandName(value.command)) {
return { ok: false, reason: 'Bridge result has unsupported command' };
}
if (typeof value.completedAt !== 'string' || !value.completedAt.trim()) {
return { ok: false, reason: 'Bridge result missing completedAt' };
}
if (!isNonNegativeFiniteNumber(value.durationMs)) {
return { ok: false, reason: 'Bridge result has invalid durationMs' };
}
if (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnosticEvent)) {
return { ok: false, reason: 'Bridge result diagnostics are invalid' };
}
if (value.ok) {
if (!isRuntimeSnapshot(value.runtime)) {
return { ok: false, reason: 'Bridge success runtime snapshot is invalid' };
}
if (!Object.prototype.hasOwnProperty.call(value, 'data')) {
return { ok: false, reason: 'Bridge success missing data' };
}
return { ok: true, value: value as unknown as OpenCodeBridgeSuccess<unknown> };
}
if (!isRecord(value.error)) {
return { ok: false, reason: 'Bridge failure missing error object' };
}
if (!VALID_FAILURE_KINDS.has(value.error.kind as OpenCodeBridgeFailureKind)) {
return { ok: false, reason: 'Bridge failure has unsupported error kind' };
}
if (typeof value.error.message !== 'string' || !value.error.message.trim()) {
return { ok: false, reason: 'Bridge failure missing error message' };
}
if (typeof value.error.retryable !== 'boolean') {
return { ok: false, reason: 'Bridge failure missing retryable boolean' };
}
if (
value.error.details !== undefined &&
(value.error.details === null || !isRecord(value.error.details))
) {
return { ok: false, reason: 'Bridge failure details must be an object' };
}
return { ok: true, value: value as unknown as OpenCodeBridgeFailure };
}
function validateOpenCodeBridgeHandshakeShape(
handshake: OpenCodeBridgeHandshake
): { ok: true } | { ok: false; reason: string } {
if (!isRecord(handshake)) {
return { ok: false, reason: 'Bridge handshake must be a JSON object' };
}
if (handshake.schemaVersion !== OPEN_CODE_BRIDGE_SCHEMA_VERSION) {
return { ok: false, reason: 'Bridge handshake has unsupported schemaVersion' };
}
if (typeof handshake.requestId !== 'string' || !handshake.requestId.trim()) {
return { ok: false, reason: 'Bridge handshake missing requestId' };
}
if (!isPeerIdentity(handshake.client) || !isPeerIdentity(handshake.server)) {
return { ok: false, reason: 'Bridge handshake peer identity is invalid' };
}
if (!Number.isInteger(handshake.agreedProtocolVersion) || handshake.agreedProtocolVersion < 1) {
return { ok: false, reason: 'Bridge handshake protocol version is invalid' };
}
if (
!Array.isArray(handshake.acceptedCommands) ||
!handshake.acceptedCommands.every(isOpenCodeBridgeCommandName)
) {
return { ok: false, reason: 'Bridge handshake accepted commands are invalid' };
}
if (typeof handshake.serverTime !== 'string' || !handshake.serverTime.trim()) {
return { ok: false, reason: 'Bridge handshake serverTime is invalid' };
}
if (typeof handshake.identityHash !== 'string' || !handshake.identityHash.trim()) {
return { ok: false, reason: 'Bridge handshake identityHash is invalid' };
}
return { ok: true };
}
function isPeerIdentity(value: unknown): value is OpenCodeBridgePeerIdentity {
if (!isRecord(value)) {
return false;
}
if (
value.schemaVersion !== OPEN_CODE_BRIDGE_SCHEMA_VERSION ||
(value.peer !== 'claude_team' && value.peer !== 'agent_teams_orchestrator') ||
typeof value.appVersion !== 'string' ||
!isNullableString(value.gitSha) ||
!isNullableString(value.buildId)
) {
return false;
}
const bridgeProtocol = value.bridgeProtocol;
if (!isRecord(bridgeProtocol)) {
return false;
}
if (
!Number.isInteger(bridgeProtocol.minVersion) ||
!Number.isInteger(bridgeProtocol.currentVersion) ||
(bridgeProtocol.minVersion as number) < 1 ||
(bridgeProtocol.currentVersion as number) < (bridgeProtocol.minVersion as number) ||
!Array.isArray(bridgeProtocol.supportedCommands) ||
!bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName)
) {
return false;
}
const runtime = value.runtime;
if (!isRecord(runtime) || runtime.providerId !== 'opencode') {
return false;
}
if (
!isNullableString(runtime.binaryPath) ||
!isNullableString(runtime.binaryFingerprint) ||
!isNullableString(runtime.version) ||
!isNullableString(runtime.capabilitySnapshotId) ||
!isNullableInteger(runtime.runtimeStoreManifestHighWatermark) ||
!isNullableString(runtime.activeRunId)
) {
return false;
}
const featureFlags = value.featureFlags;
if (!isRecord(featureFlags)) {
return false;
}
return (
typeof featureFlags.opencodeTeamLaunch === 'boolean' &&
typeof featureFlags.opencodeStateChangingCommands === 'boolean'
);
}
function isRuntimeSnapshot(value: unknown): value is OpenCodeBridgeRuntimeSnapshot {
return (
isRecord(value) &&
value.providerId === 'opencode' &&
isNullableString(value.binaryPath) &&
isNullableString(value.binaryFingerprint) &&
isNullableString(value.version) &&
isNullableString(value.capabilitySnapshotId)
);
}
function isDiagnosticEvent(value: unknown): value is OpenCodeBridgeDiagnosticEvent {
return (
isRecord(value) &&
value.providerId === 'opencode' &&
typeof value.type === 'string' &&
value.type.trim().length > 0 &&
(value.severity === 'info' || value.severity === 'warning' || value.severity === 'error') &&
typeof value.message === 'string' &&
value.message.trim().length > 0 &&
typeof value.createdAt === 'string' &&
value.createdAt.trim().length > 0 &&
(value.data === undefined || isRecord(value.data))
);
}
function extractStringByPath(value: unknown, pathParts: string[]): string | null {
const nested = getByPath(value, pathParts);
return typeof nested === 'string' && nested.trim() ? nested : null;
}
function extractNumberByPath(value: unknown, pathParts: string[]): number | null {
const nested = getByPath(value, pathParts);
return isNonNegativeFiniteNumber(nested) ? nested : null;
}
function getByPath(value: unknown, pathParts: string[]): unknown {
let current = value;
for (const part of pathParts) {
if (!isRecord(current)) {
return undefined;
}
current = current[part];
}
return current;
}
function sanitizeKeyPart(value: string): string {
const sanitized = value
.trim()
.replace(/[^a-zA-Z0-9_.-]+/g, '_')
.replace(/^_+|_+$/g, '');
return sanitized.slice(0, 64) || 'unknown';
}
function stableJsonComparableNumber(value: number): number | string {
if (Number.isFinite(value)) {
return value;
}
return String(value);
}
function normalizeStableJson(value: unknown): unknown {
if (value === null) {
return null;
}
if (typeof value === 'number') {
return stableJsonComparableNumber(value);
}
if (typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
return value.map(normalizeStableJson);
}
const output: Record<string, unknown> = {};
for (const key of Object.keys(value).sort()) {
const nested = (value as Record<string, unknown>)[key];
if (nested !== undefined) {
output[key] = normalizeStableJson(nested);
}
}
return output;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNullableString(value: unknown): value is string | null {
return value === null || typeof value === 'string';
}
function isNullableInteger(value: unknown): value is number | null {
return value === null || (Number.isInteger(value) && (value as number) >= 0);
}
function isNonNegativeFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,438 @@
import {
createOpenCodeBridgeIdempotencyKey,
isOpenCodeBridgeCommandName,
stableHash,
type OpenCodeBridgeCommandName,
} from './OpenCodeBridgeCommandContract';
import { VersionedJsonStore, VersionedJsonStoreError } from '../store/VersionedJsonStore';
export const OPEN_CODE_BRIDGE_COMMAND_LEDGER_SCHEMA_VERSION = 1;
export const OPEN_CODE_BRIDGE_COMMAND_LEASE_SCHEMA_VERSION = 1;
export type OpenCodeBridgeCommandLedgerStatus =
| 'started'
| 'completed'
| 'failed'
| 'unknown_after_timeout';
export interface OpenCodeBridgeCommandLedgerEntry {
idempotencyKey: string;
requestId: string;
command: OpenCodeBridgeCommandName;
teamName: string;
runId: string | null;
requestHash: string;
responseHash: string | null;
status: OpenCodeBridgeCommandLedgerStatus;
retryable: boolean;
startedAt: string;
completedAt: string | null;
lastError: string | null;
}
export interface OpenCodeBridgeCommandLease {
leaseId: string;
teamName: string;
runId: string | null;
command: OpenCodeBridgeCommandName;
holderPeer: 'claude_team';
acquiredAt: string;
expiresAt: string;
state: 'active' | 'released' | 'expired';
}
export type OpenCodeBridgeLedgerBeginResult = 'started' | 'duplicate_same_payload_completed';
export class OpenCodeBridgeCommandLedgerError extends Error {
constructor(message: string) {
super(message);
this.name = 'OpenCodeBridgeCommandLedgerError';
}
}
export class OpenCodeBridgeCommandLeaseError extends Error {
constructor(message: string) {
super(message);
this.name = 'OpenCodeBridgeCommandLeaseError';
}
}
export class OpenCodeBridgeCommandLedger {
constructor(
private readonly store: VersionedJsonStore<OpenCodeBridgeCommandLedgerEntry[]>,
private readonly clock: () => Date = () => new Date()
) {}
async begin(input: {
idempotencyKey: string;
requestId: string;
command: OpenCodeBridgeCommandName;
teamName: string;
runId: string | null;
requestHash: string;
}): Promise<OpenCodeBridgeLedgerBeginResult> {
let outcome: OpenCodeBridgeLedgerBeginResult = 'started';
await this.store.updateLocked((entries) => {
const existing = entries.find((entry) => entry.idempotencyKey === input.idempotencyKey);
if (existing) {
if (existing.requestHash !== input.requestHash) {
throw new OpenCodeBridgeCommandLedgerError(
'OpenCode bridge idempotency key reused with different payload'
);
}
if (existing.status === 'unknown_after_timeout') {
throw new OpenCodeBridgeCommandLedgerError(
'OpenCode bridge command outcome must be reconciled before retry'
);
}
if (existing.status === 'started') {
throw new OpenCodeBridgeCommandLedgerError('OpenCode bridge command already started');
}
if (existing.status === 'completed') {
outcome = 'duplicate_same_payload_completed';
return entries;
}
throw new OpenCodeBridgeCommandLedgerError(
`OpenCode bridge command cannot be retried from status ${existing.status}`
);
}
const now = this.clock().toISOString();
return [
...entries,
{
idempotencyKey: input.idempotencyKey,
requestId: input.requestId,
command: input.command,
teamName: input.teamName,
runId: input.runId,
requestHash: input.requestHash,
responseHash: null,
status: 'started',
retryable: false,
startedAt: now,
completedAt: null,
lastError: null,
},
];
});
return outcome;
}
async markCompleted(input: {
idempotencyKey: string;
response: unknown;
completedAt?: Date;
}): Promise<void> {
await this.updateExisting(input.idempotencyKey, (entry) => ({
...entry,
responseHash: stableHash(input.response),
status: 'completed',
retryable: false,
completedAt: (input.completedAt ?? this.clock()).toISOString(),
lastError: null,
}));
}
async markFailed(input: {
idempotencyKey: string;
error: string;
retryable: boolean;
completedAt?: Date;
}): Promise<void> {
await this.updateExisting(input.idempotencyKey, (entry) => ({
...entry,
status: 'failed',
retryable: input.retryable,
completedAt: (input.completedAt ?? this.clock()).toISOString(),
lastError: input.error,
}));
}
async markUnknownAfterTimeout(input: { idempotencyKey: string; error: string }): Promise<void> {
await this.updateExisting(input.idempotencyKey, (entry) => ({
...entry,
status: 'unknown_after_timeout',
retryable: false,
completedAt: null,
lastError: input.error,
}));
}
async getByIdempotencyKey(
idempotencyKey: string
): Promise<OpenCodeBridgeCommandLedgerEntry | null> {
const entries = await this.readRequired();
return entries.find((entry) => entry.idempotencyKey === idempotencyKey) ?? null;
}
async list(): Promise<OpenCodeBridgeCommandLedgerEntry[]> {
return this.readRequired();
}
private async updateExisting(
idempotencyKey: string,
updater: (entry: OpenCodeBridgeCommandLedgerEntry) => OpenCodeBridgeCommandLedgerEntry
): Promise<void> {
let found = false;
await this.store.updateLocked((entries) =>
entries.map((entry) => {
if (entry.idempotencyKey !== idempotencyKey) {
return entry;
}
found = true;
return updater(entry);
})
);
if (!found) {
throw new OpenCodeBridgeCommandLedgerError(
`OpenCode bridge command ledger entry not found: ${idempotencyKey}`
);
}
}
private async readRequired(): Promise<OpenCodeBridgeCommandLedgerEntry[]> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
return result.data;
}
}
export class OpenCodeBridgeCommandLeaseStore {
constructor(
private readonly store: VersionedJsonStore<OpenCodeBridgeCommandLease[]>,
private readonly idFactory: () => string,
private readonly clock: () => Date = () => new Date()
) {}
async acquire(input: {
teamName: string;
runId: string | null;
command: OpenCodeBridgeCommandName;
ttlMs: number;
}): Promise<OpenCodeBridgeCommandLease> {
let created: OpenCodeBridgeCommandLease | null = null;
await this.store.updateLocked((leases) => {
const now = this.clock();
const nowMs = now.getTime();
const normalized = leases.map((lease) =>
lease.state === 'active' && Date.parse(lease.expiresAt) <= nowMs
? { ...lease, state: 'expired' as const }
: lease
);
const active = normalized.find(
(lease) =>
lease.teamName === input.teamName &&
lease.state === 'active' &&
Date.parse(lease.expiresAt) > nowMs
);
if (active) {
throw new OpenCodeBridgeCommandLeaseError(
`OpenCode bridge command lease already active: ${active.leaseId}`
);
}
created = {
leaseId: this.idFactory(),
teamName: input.teamName,
runId: input.runId,
command: input.command,
holderPeer: 'claude_team',
acquiredAt: now.toISOString(),
expiresAt: new Date(nowMs + input.ttlMs).toISOString(),
state: 'active',
};
return [...normalized, created];
});
if (!created) {
throw new OpenCodeBridgeCommandLeaseError('OpenCode bridge command lease was not created');
}
return created;
}
async release(leaseId: string): Promise<void> {
let found = false;
await this.store.updateLocked((leases) =>
leases.map((lease) => {
if (lease.leaseId !== leaseId) {
return lease;
}
found = true;
return { ...lease, state: 'released' as const };
})
);
if (!found) {
throw new OpenCodeBridgeCommandLeaseError(
`OpenCode bridge command lease not found: ${leaseId}`
);
}
}
async getActive(teamName: string): Promise<OpenCodeBridgeCommandLease | null> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
const nowMs = this.clock().getTime();
return (
result.data.find(
(lease) =>
lease.teamName === teamName &&
lease.state === 'active' &&
Date.parse(lease.expiresAt) > nowMs
) ?? null
);
}
}
export function createOpenCodeBridgeCommandLedgerStore(options: {
filePath: string;
clock?: () => Date;
}): OpenCodeBridgeCommandLedger {
const clock = options.clock ?? (() => new Date());
return new OpenCodeBridgeCommandLedger(
new VersionedJsonStore<OpenCodeBridgeCommandLedgerEntry[]>({
filePath: options.filePath,
schemaVersion: OPEN_CODE_BRIDGE_COMMAND_LEDGER_SCHEMA_VERSION,
defaultData: () => [],
validate: validateLedgerEntries,
clock,
}),
clock
);
}
export function createOpenCodeBridgeCommandLeaseStore(options: {
filePath: string;
idFactory?: () => string;
clock?: () => Date;
}): OpenCodeBridgeCommandLeaseStore {
const clock = options.clock ?? (() => new Date());
return new OpenCodeBridgeCommandLeaseStore(
new VersionedJsonStore<OpenCodeBridgeCommandLease[]>({
filePath: options.filePath,
schemaVersion: OPEN_CODE_BRIDGE_COMMAND_LEASE_SCHEMA_VERSION,
defaultData: () => [],
validate: validateLeases,
clock,
}),
options.idFactory ??
(() =>
createOpenCodeBridgeIdempotencyKey({
command: 'opencode.commandStatus',
teamName: 'lease',
runId: null,
body: { now: clock().toISOString(), random: Math.random() },
})),
clock
);
}
export function validateLedgerEntries(value: unknown): OpenCodeBridgeCommandLedgerEntry[] {
if (!Array.isArray(value)) {
throw new Error('OpenCode bridge command ledger must be an array');
}
const seen = new Set<string>();
return value.map((entry, index) => {
if (!isLedgerEntry(entry)) {
throw new Error(`Invalid OpenCode bridge command ledger entry at index ${index}`);
}
if (seen.has(entry.idempotencyKey)) {
throw new Error(`Duplicate OpenCode bridge ledger idempotencyKey at index ${index}`);
}
seen.add(entry.idempotencyKey);
return entry;
});
}
export function validateLeases(value: unknown): OpenCodeBridgeCommandLease[] {
if (!Array.isArray(value)) {
throw new Error('OpenCode bridge command leases must be an array');
}
const seen = new Set<string>();
return value.map((lease, index) => {
if (!isLease(lease)) {
throw new Error(`Invalid OpenCode bridge command lease at index ${index}`);
}
if (seen.has(lease.leaseId)) {
throw new Error(`Duplicate OpenCode bridge leaseId at index ${index}`);
}
seen.add(lease.leaseId);
return lease;
});
}
function isLedgerEntry(value: unknown): value is OpenCodeBridgeCommandLedgerEntry {
return (
isRecord(value) &&
isNonEmptyString(value.idempotencyKey) &&
isNonEmptyString(value.requestId) &&
isOpenCodeBridgeCommandName(value.command) &&
isNonEmptyString(value.teamName) &&
isNullableString(value.runId) &&
isNonEmptyString(value.requestHash) &&
isNullableString(value.responseHash) &&
isLedgerStatus(value.status) &&
typeof value.retryable === 'boolean' &&
isNonEmptyString(value.startedAt) &&
isNullableString(value.completedAt) &&
isNullableString(value.lastError) &&
Number.isFinite(Date.parse(value.startedAt)) &&
(value.completedAt === null || Number.isFinite(Date.parse(value.completedAt)))
);
}
function isLease(value: unknown): value is OpenCodeBridgeCommandLease {
return (
isRecord(value) &&
isNonEmptyString(value.leaseId) &&
isNonEmptyString(value.teamName) &&
isNullableString(value.runId) &&
isOpenCodeBridgeCommandName(value.command) &&
value.holderPeer === 'claude_team' &&
isNonEmptyString(value.acquiredAt) &&
isNonEmptyString(value.expiresAt) &&
Number.isFinite(Date.parse(value.acquiredAt)) &&
Number.isFinite(Date.parse(value.expiresAt)) &&
(value.state === 'active' || value.state === 'released' || value.state === 'expired')
);
}
function isLedgerStatus(value: unknown): value is OpenCodeBridgeCommandLedgerStatus {
return (
value === 'started' ||
value === 'completed' ||
value === 'failed' ||
value === 'unknown_after_timeout'
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function isNullableString(value: unknown): value is string | null {
return value === null || typeof value === 'string';
}

View file

@ -0,0 +1,112 @@
import type {
OpenCodeBridgeCommandName,
OpenCodeBridgeHandshake,
OpenCodeBridgePeerIdentity,
} from './OpenCodeBridgeCommandContract';
import type {
OpenCodeBridgeCommandExecutor,
OpenCodeBridgeHandshakePort,
} from './OpenCodeStateChangingBridgeCommandService';
export interface OpenCodeBridgeCommandHandshakePortOptions {
bridge: OpenCodeBridgeCommandExecutor;
clientIdentity: OpenCodeBridgePeerIdentity;
timeoutMs?: number;
}
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 120_000;
export class OpenCodeBridgeCommandHandshakePort implements OpenCodeBridgeHandshakePort {
private readonly bridge: OpenCodeBridgeCommandExecutor;
private readonly clientIdentity: OpenCodeBridgePeerIdentity;
private readonly timeoutMs: number;
constructor(options: OpenCodeBridgeCommandHandshakePortOptions) {
this.bridge = options.bridge;
this.clientIdentity = options.clientIdentity;
this.timeoutMs = options.timeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
}
async handshake(input: {
requiredCommand: OpenCodeBridgeCommandName;
expectedRunId: string | null;
expectedCapabilitySnapshotId: string | null;
expectedManifestHighWatermark: number | null;
cwd?: string;
}): Promise<OpenCodeBridgeHandshake> {
const result = await this.bridge.execute<
{
client: OpenCodeBridgePeerIdentity;
requiredCommand: OpenCodeBridgeCommandName;
expectedRunId: string | null;
expectedCapabilitySnapshotId: string | null;
expectedManifestHighWatermark: number | null;
},
OpenCodeBridgeHandshake
>(
'opencode.handshake',
{
client: this.clientIdentity,
requiredCommand: input.requiredCommand,
expectedRunId: input.expectedRunId,
expectedCapabilitySnapshotId: input.expectedCapabilitySnapshotId,
expectedManifestHighWatermark: input.expectedManifestHighWatermark,
},
{
cwd: input.cwd ?? process.cwd(),
timeoutMs: this.timeoutMs,
}
);
if (!result.ok) {
throw new Error(
`OpenCode bridge handshake failed: ${result.error.kind}: ${result.error.message}`
);
}
return result.data;
}
}
export function createOpenCodeBridgeClientIdentity(input: {
appVersion: string;
gitSha?: string | null;
buildId?: string | null;
}): OpenCodeBridgePeerIdentity {
return {
schemaVersion: 1,
peer: 'claude_team',
appVersion: input.appVersion,
gitSha: input.gitSha ?? null,
buildId: input.buildId ?? null,
bridgeProtocol: {
minVersion: 1,
currentVersion: 1,
supportedCommands: [
'opencode.handshake',
'opencode.commandStatus',
'opencode.readiness',
'opencode.launchTeam',
'opencode.reconcileTeam',
'opencode.stopTeam',
'opencode.answerPermission',
'opencode.listRuntimePermissions',
'opencode.getRuntimeTranscript',
'opencode.recoverDeliveryJournal',
],
},
runtime: {
providerId: 'opencode',
binaryPath: null,
binaryFingerprint: null,
version: null,
capabilitySnapshotId: null,
runtimeStoreManifestHighWatermark: null,
activeRunId: null,
},
featureFlags: {
opencodeTeamLaunch: true,
opencodeStateChangingCommands: true,
},
};
}

View file

@ -0,0 +1,413 @@
import type { OpenCodeTeamRuntimeBridgePort } from '../../runtime/OpenCodeTeamRuntimeAdapter';
import {
buildOpenCodeCanonicalMcpToolId,
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
} from '../mcp/OpenCodeMcpToolAvailability';
import type {
OpenCodeTeamLaunchReadiness,
OpenCodeTeamLaunchReadinessState,
} from '../readiness/OpenCodeTeamLaunchReadiness';
import {
assertOpenCodeProductionE2EArtifactGate,
type OpenCodeProductionE2EEvidence,
} from '../e2e/OpenCodeProductionE2EEvidence';
import type {
OpenCodeBridgeCommandName,
OpenCodeBridgeDiagnosticEvent,
OpenCodeBridgeFailureKind,
OpenCodeBridgeResult,
OpenCodeBridgeRuntimeSnapshot,
OpenCodeLaunchTeamCommandBody,
OpenCodeLaunchTeamCommandData,
OpenCodeReconcileTeamCommandBody,
OpenCodeStopTeamCommandBody,
OpenCodeStopTeamCommandData,
OpenCodeTeamLaunchMode,
} from './OpenCodeBridgeCommandContract';
import type { OpenCodeStateChangingBridgeCommandService } from './OpenCodeStateChangingBridgeCommandService';
export interface OpenCodeReadinessBridgeCommandExecutor {
execute<TBody, TData>(
command: OpenCodeBridgeCommandName,
body: TBody,
options: {
cwd: string;
timeoutMs: number;
requestId?: string;
stdoutLimitBytes?: number;
stderrLimitBytes?: number;
}
): Promise<OpenCodeBridgeResult<TData>>;
}
export interface OpenCodeReadinessBridgeOptions {
timeoutMs?: number;
launchTimeoutMs?: number;
reconcileTimeoutMs?: number;
stopTimeoutMs?: number;
stateChangingCommands?: Pick<OpenCodeStateChangingBridgeCommandService, 'execute'>;
productionE2eEvidence?: OpenCodeProductionE2EEvidenceReadPort;
}
export interface OpenCodeProductionE2EEvidenceReadPort {
read(): Promise<{
ok: boolean;
evidence: OpenCodeProductionE2EEvidence | null;
artifactPath: string;
diagnostics: string[];
}>;
}
export interface OpenCodeReadinessBridgeCommandBody {
projectPath: string;
selectedModel: string | null;
requireExecutionProbe: boolean;
launchMode?: OpenCodeTeamLaunchMode;
}
const DEFAULT_READINESS_TIMEOUT_MS = 120_000;
const DEFAULT_LAUNCH_TIMEOUT_MS = 120_000;
const DEFAULT_RECONCILE_TIMEOUT_MS = 30_000;
const DEFAULT_STOP_TIMEOUT_MS = 30_000;
export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort {
private readonly lastRuntimeSnapshotsByProjectPath = new Map<
string,
OpenCodeBridgeRuntimeSnapshot
>();
constructor(
private readonly bridge: OpenCodeReadinessBridgeCommandExecutor,
private readonly options: OpenCodeReadinessBridgeOptions = {}
) {}
async checkOpenCodeTeamLaunchReadiness(
input: OpenCodeReadinessBridgeCommandBody
): Promise<OpenCodeTeamLaunchReadiness> {
const result = await this.bridge.execute<
OpenCodeReadinessBridgeCommandBody,
OpenCodeTeamLaunchReadiness
>('opencode.readiness', input, {
cwd: input.projectPath,
timeoutMs: this.options.timeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS,
});
if (result.ok) {
this.lastRuntimeSnapshotsByProjectPath.set(input.projectPath, result.runtime);
return this.applyProductionE2EGate({
input,
readiness: result.data,
runtime: result.runtime,
});
}
this.lastRuntimeSnapshotsByProjectPath.delete(input.projectPath);
return blockedReadiness({
state: mapBridgeFailureToReadinessState(result.error.kind),
modelId: input.selectedModel,
diagnostics: [
`OpenCode readiness bridge failed: ${result.error.kind}: ${result.error.message}`,
...result.diagnostics.map(formatDiagnosticEvent),
],
missing: [result.error.message],
});
}
private async applyProductionE2EGate(input: {
input: OpenCodeReadinessBridgeCommandBody;
readiness: OpenCodeTeamLaunchReadiness;
runtime: OpenCodeBridgeRuntimeSnapshot;
}): Promise<OpenCodeTeamLaunchReadiness> {
const launchMode = input.input.launchMode;
if (launchMode !== 'production' && launchMode !== 'dogfood') {
return input.readiness;
}
if (!input.readiness.launchAllowed) {
return input.readiness;
}
const evidenceRead = this.options.productionE2eEvidence
? await this.options.productionE2eEvidence.read()
: {
ok: false,
evidence: null,
artifactPath: '',
diagnostics: ['OpenCode production E2E evidence store is not configured'],
};
const expectedModel = input.readiness.modelId ?? input.input.selectedModel;
const gate = evidenceRead.ok
? assertOpenCodeProductionE2EArtifactGate({
evidence: evidenceRead.evidence,
artifactPath: evidenceRead.artifactPath,
expected: {
opencodeVersion: input.runtime.version,
binaryFingerprint: input.runtime.binaryFingerprint,
capabilitySnapshotId: input.runtime.capabilitySnapshotId,
selectedModel: expectedModel,
requiredMcpTools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
),
},
})
: {
ok: false,
diagnostics: evidenceRead.diagnostics,
};
if (gate.ok) {
return {
...input.readiness,
diagnostics: dedupe([...input.readiness.diagnostics, ...evidenceRead.diagnostics]),
supportLevel: 'production_supported',
};
}
const diagnostics = dedupe([
...input.readiness.diagnostics,
...evidenceRead.diagnostics,
...gate.diagnostics,
]);
if (launchMode === 'dogfood') {
return {
...input.readiness,
supportLevel: 'supported_e2e_pending',
diagnostics,
};
}
return {
...input.readiness,
state: 'e2e_missing',
launchAllowed: false,
supportLevel: 'supported_e2e_pending',
missing: dedupe([...input.readiness.missing, ...gate.diagnostics]),
diagnostics,
};
}
getLastOpenCodeRuntimeSnapshot(projectPath: string): OpenCodeBridgeRuntimeSnapshot | null {
return this.lastRuntimeSnapshotsByProjectPath.get(projectPath) ?? null;
}
async launchOpenCodeTeam(
input: OpenCodeLaunchTeamCommandBody
): Promise<OpenCodeLaunchTeamCommandData> {
const result = await this.executeStateChangingCommand<
OpenCodeLaunchTeamCommandBody,
OpenCodeLaunchTeamCommandData
>('opencode.launchTeam', input, {
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: input.expectedCapabilitySnapshotId,
cwd: input.projectPath,
timeoutMs: this.options.launchTimeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS,
});
return result.ok ? result.data : blockedLaunchData(input.runId, result);
}
async reconcileOpenCodeTeam(
input: OpenCodeReconcileTeamCommandBody
): Promise<OpenCodeLaunchTeamCommandData> {
const cwd = input.projectPath ?? process.cwd();
const result = await this.executeStateChangingCommand<
OpenCodeReconcileTeamCommandBody,
OpenCodeLaunchTeamCommandData
>('opencode.reconcileTeam', input, {
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: input.expectedCapabilitySnapshotId ?? null,
cwd,
timeoutMs: this.options.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS,
});
return result.ok ? result.data : blockedLaunchData(input.runId, result);
}
async stopOpenCodeTeam(input: OpenCodeStopTeamCommandBody): Promise<OpenCodeStopTeamCommandData> {
const cwd = input.projectPath ?? process.cwd();
const result = await this.executeStateChangingCommand<
OpenCodeStopTeamCommandBody,
OpenCodeStopTeamCommandData
>('opencode.stopTeam', input, {
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: input.expectedCapabilitySnapshotId ?? null,
cwd,
timeoutMs: this.options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS,
});
if (result.ok) {
return result.data;
}
return {
runId: input.runId,
stopped: false,
members: {},
warnings: [],
diagnostics: [
{
code: result.error.kind,
severity: 'error',
message: `OpenCode stop bridge failed: ${result.error.message}`,
},
...result.diagnostics.map((event) => ({
code: event.type,
severity: event.severity,
message: event.message,
})),
],
};
}
private async executeStateChangingCommand<TBody, TData>(
command: OpenCodeStateChangingTeamCommandName,
body: TBody,
input: {
teamName: string;
runId: string;
capabilitySnapshotId: string | null;
cwd: string;
timeoutMs: number;
}
): Promise<OpenCodeBridgeResult<TData>> {
if (this.options.stateChangingCommands) {
try {
return await this.options.stateChangingCommands.execute<TBody, TData>({
command,
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: input.capabilitySnapshotId,
behaviorFingerprint: null,
body,
cwd: input.cwd,
timeoutMs: input.timeoutMs,
});
} catch (error) {
return thrownBridgeFailure(command, input.runId, error);
}
}
return this.bridge.execute<TBody, TData>(command, body, {
cwd: input.cwd,
timeoutMs: input.timeoutMs,
});
}
}
type OpenCodeStateChangingTeamCommandName = Extract<
OpenCodeBridgeCommandName,
'opencode.launchTeam' | 'opencode.reconcileTeam' | 'opencode.stopTeam'
>;
function blockedLaunchData(
runId: string,
result: OpenCodeBridgeResult<unknown>
): OpenCodeLaunchTeamCommandData {
if (result.ok) {
throw new Error('blockedLaunchData expects a failed bridge result');
}
return {
runId,
teamLaunchState: 'failed',
members: {},
warnings: [],
diagnostics: [
{
code: result.error.kind,
severity: 'error',
message: `OpenCode bridge failed: ${result.error.message}`,
},
...result.diagnostics.map((event) => ({
code: event.type,
severity: event.severity,
message: event.message,
})),
],
};
}
function blockedReadiness(input: {
state: OpenCodeTeamLaunchReadinessState;
modelId: string | null;
diagnostics: string[];
missing: string[];
}): OpenCodeTeamLaunchReadiness {
return {
state: input.state,
launchAllowed: false,
modelId: input.modelId,
opencodeVersion: null,
installMethod: null,
binaryPath: null,
hostHealthy: false,
appMcpConnected: false,
requiredToolsPresent: false,
permissionBridgeReady: false,
runtimeStoresReady: false,
supportLevel: null,
missing: dedupe(input.missing),
diagnostics: dedupe(input.diagnostics),
evidence: {
capabilitiesReady: false,
mcpToolProofRoute: null,
observedMcpTools: [],
runtimeStoreReadinessReason: null,
},
};
}
function mapBridgeFailureToReadinessState(
kind: OpenCodeBridgeFailureKind
): OpenCodeTeamLaunchReadinessState {
switch (kind) {
case 'runtime_not_ready':
return 'adapter_disabled';
case 'timeout':
case 'contract_violation':
case 'provider_error':
case 'unsupported_schema':
case 'unsupported_command':
case 'invalid_input':
case 'internal_error':
default:
return 'unknown_error';
}
}
function formatDiagnosticEvent(event: OpenCodeBridgeDiagnosticEvent): string {
return `${event.type}: ${event.message}`;
}
function thrownBridgeFailure<TData>(
command: OpenCodeBridgeCommandName,
runId: string,
error: unknown
): OpenCodeBridgeResult<TData> {
const message = error instanceof Error ? error.message : String(error);
const completedAt = new Date().toISOString();
return {
ok: false,
schemaVersion: 1,
requestId: 'opencode-state-changing-bridge-exception',
command,
completedAt,
durationMs: 0,
error: {
kind: 'internal_error',
message,
retryable: false,
},
diagnostics: [
{
type: 'opencode_state_changing_bridge_exception',
providerId: 'opencode',
runId,
severity: 'error',
message,
createdAt: completedAt,
},
],
};
}
function dedupe(values: string[]): string[] {
return [...new Set(values.filter((value) => value.trim().length > 0))];
}

View file

@ -0,0 +1,283 @@
import { randomUUID } from 'crypto';
import {
assertBridgeEvidenceCanCommitToRuntimeStores,
createOpenCodeBridgeIdempotencyKey,
extractRunId,
stableHash,
validateOpenCodeBridgeHandshake,
type OpenCodeBridgeCommandName,
type OpenCodeBridgeCommandPreconditions,
type OpenCodeBridgeDiagnosticEvent,
type OpenCodeBridgeHandshake,
type OpenCodeBridgePeerIdentity,
type OpenCodeBridgeResult,
type RuntimeStoreManifestEvidence,
} from './OpenCodeBridgeCommandContract';
import {
OpenCodeBridgeCommandLedger,
OpenCodeBridgeCommandLeaseStore,
} from './OpenCodeBridgeCommandLedgerStore';
export interface OpenCodeBridgeCommandExecutor {
execute<TBody, TData>(
command: OpenCodeBridgeCommandName,
body: TBody,
options: {
cwd: string;
timeoutMs: number;
requestId?: string;
stdoutLimitBytes?: number;
stderrLimitBytes?: number;
}
): Promise<OpenCodeBridgeResult<TData>>;
}
export interface OpenCodeBridgeHandshakePort {
handshake(input: {
requiredCommand: OpenCodeBridgeCommandName;
expectedRunId: string | null;
expectedCapabilitySnapshotId: string | null;
expectedManifestHighWatermark: number | null;
cwd?: string;
}): Promise<OpenCodeBridgeHandshake>;
}
export interface RuntimeStoreManifestReader {
read(teamName: string): Promise<RuntimeStoreManifestEvidence>;
}
export interface OpenCodeStateChangingBridgeDiagnosticsSink {
append(event: OpenCodeBridgeDiagnosticEvent): Promise<void>;
}
export interface OpenCodeStateChangingBridgeCommandServiceOptions {
expectedClientIdentity: OpenCodeBridgePeerIdentity;
handshakePort: OpenCodeBridgeHandshakePort;
leaseStore: OpenCodeBridgeCommandLeaseStore;
ledger: OpenCodeBridgeCommandLedger;
bridge: OpenCodeBridgeCommandExecutor;
manifestReader: RuntimeStoreManifestReader;
diagnostics?: OpenCodeStateChangingBridgeDiagnosticsSink;
requestIdFactory?: () => string;
diagnosticIdFactory?: () => string;
clock?: () => Date;
}
export class OpenCodeStateChangingBridgeCommandService {
private readonly expectedClientIdentity: OpenCodeBridgePeerIdentity;
private readonly handshakePort: OpenCodeBridgeHandshakePort;
private readonly leaseStore: OpenCodeBridgeCommandLeaseStore;
private readonly ledger: OpenCodeBridgeCommandLedger;
private readonly bridge: OpenCodeBridgeCommandExecutor;
private readonly manifestReader: RuntimeStoreManifestReader;
private readonly diagnostics: OpenCodeStateChangingBridgeDiagnosticsSink | null;
private readonly requestIdFactory: () => string;
private readonly diagnosticIdFactory: () => string;
private readonly clock: () => Date;
constructor(options: OpenCodeStateChangingBridgeCommandServiceOptions) {
this.expectedClientIdentity = options.expectedClientIdentity;
this.handshakePort = options.handshakePort;
this.leaseStore = options.leaseStore;
this.ledger = options.ledger;
this.bridge = options.bridge;
this.manifestReader = options.manifestReader;
this.diagnostics = options.diagnostics ?? null;
this.requestIdFactory = options.requestIdFactory ?? (() => `opencode-bridge-${randomUUID()}`);
this.diagnosticIdFactory =
options.diagnosticIdFactory ?? (() => `opencode-bridge-diagnostic-${randomUUID()}`);
this.clock = options.clock ?? (() => new Date());
}
async execute<TBody, TData>(input: {
command: OpenCodeBridgeCommandName;
teamName: string;
runId: string | null;
capabilitySnapshotId: string | null;
behaviorFingerprint: string | null;
body: TBody;
cwd: string;
timeoutMs: number;
}): Promise<OpenCodeBridgeResult<TData>> {
const manifest = await this.manifestReader.read(input.teamName);
const handshake = await this.handshakePort.handshake({
requiredCommand: input.command,
expectedRunId: input.runId,
expectedCapabilitySnapshotId: input.capabilitySnapshotId,
expectedManifestHighWatermark: manifest.highWatermark,
cwd: input.cwd,
});
const handshakeValidation = validateOpenCodeBridgeHandshake({
handshake,
expectedClient: this.expectedClientIdentity,
requiredCommand: input.command,
expectedCapabilitySnapshotId: input.capabilitySnapshotId,
expectedManifestHighWatermark: manifest.highWatermark,
expectedRunId: input.runId,
});
if (!handshakeValidation.ok) {
throw new Error(handshakeValidation.reason);
}
const idempotencyKey = createOpenCodeBridgeIdempotencyKey({
command: input.command,
teamName: input.teamName,
runId: input.runId,
body: input.body,
});
const commandRequestId = this.requestIdFactory();
const lease = await this.leaseStore.acquire({
teamName: input.teamName,
runId: input.runId,
command: input.command,
ttlMs: input.timeoutMs + 5_000,
});
try {
const bodyWithPreconditions = attachBridgePreconditions(input.body, {
handshakeIdentityHash: handshake.identityHash,
expectedRunId: input.runId,
expectedCapabilitySnapshotId: input.capabilitySnapshotId,
expectedBehaviorFingerprint: input.behaviorFingerprint,
expectedManifestHighWatermark: manifest.highWatermark,
commandLeaseId: lease.leaseId,
idempotencyKey,
});
const begin = await this.ledger.begin({
idempotencyKey,
requestId: commandRequestId,
command: input.command,
teamName: input.teamName,
runId: input.runId,
requestHash: stableHash({
command: input.command,
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: input.capabilitySnapshotId,
behaviorFingerprint: input.behaviorFingerprint,
manifestHighWatermark: manifest.highWatermark,
body: input.body,
}),
});
if (begin === 'duplicate_same_payload_completed') {
throw new Error('OpenCode bridge command already completed; recover through commandStatus');
}
const result = await this.bridge.execute<typeof bodyWithPreconditions, TData>(
input.command,
bodyWithPreconditions,
{
cwd: input.cwd,
timeoutMs: input.timeoutMs,
requestId: commandRequestId,
}
);
if (!result.ok) {
if (result.error.kind === 'timeout') {
await this.ledger.markUnknownAfterTimeout({
idempotencyKey,
error: result.error.message,
});
await this.appendUnknownOutcomeDiagnostic({
result,
teamName: input.teamName,
runId: input.runId,
command: input.command,
idempotencyKey,
leaseId: lease.leaseId,
});
} else {
await this.ledger.markFailed({
idempotencyKey,
error: result.error.message,
retryable: result.error.retryable,
});
}
await this.leaseStore.release(lease.leaseId);
return result;
}
try {
assertBridgeEvidenceCanCommitToRuntimeStores({
result,
requestId: commandRequestId,
command: input.command,
runId: input.runId,
capabilitySnapshotId: input.capabilitySnapshotId,
manifest,
idempotencyKey,
});
} catch (error) {
await this.ledger.markFailed({
idempotencyKey,
error: stringifyError(error),
retryable: false,
});
throw error;
}
await this.ledger.markCompleted({ idempotencyKey, response: result });
await this.leaseStore.release(lease.leaseId);
return result;
} catch (error) {
await this.leaseStore.release(lease.leaseId).catch(() => undefined);
throw error;
}
}
private async appendUnknownOutcomeDiagnostic(input: {
result: OpenCodeBridgeResult<unknown>;
teamName: string;
runId: string | null;
command: OpenCodeBridgeCommandName;
idempotencyKey: string;
leaseId: string;
}): Promise<void> {
const completedAt = this.clock().toISOString();
await this.diagnostics?.append({
id: this.diagnosticIdFactory(),
type: 'opencode_bridge_unknown_outcome',
providerId: 'opencode',
teamName: input.teamName,
runId: input.runId ?? extractRunId(input.result) ?? undefined,
severity: 'warning',
message: 'OpenCode bridge command timed out; outcome must be reconciled before retry',
data: {
command: input.command,
idempotencyKey: input.idempotencyKey,
leaseId: input.leaseId,
},
createdAt: completedAt,
});
}
}
export function attachBridgePreconditions<TBody>(
body: TBody,
preconditions: OpenCodeBridgeCommandPreconditions
): TBody & { preconditions: OpenCodeBridgeCommandPreconditions } {
if (isRecord(body)) {
return {
...body,
preconditions,
} as TBody & { preconditions: OpenCodeBridgeCommandPreconditions };
}
return {
payload: body,
preconditions,
} as unknown as TBody & { preconditions: OpenCodeBridgeCommandPreconditions };
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,555 @@
import { createHash } from 'crypto';
export interface OpenCodeApiEndpointMap {
health: boolean;
sessionCreate: boolean;
sessionGet: boolean;
sessionMessageList: boolean;
sessionPromptAsync: boolean;
sessionAbort: boolean;
sessionStatus: boolean;
permissionList: boolean;
permissionReply: boolean;
permissionLegacySessionRespond: boolean;
sessionEventStream: boolean;
globalEventStream: boolean;
mcpList: boolean;
mcpCreate: boolean;
experimentalToolIds: boolean;
experimentalToolList: boolean;
}
export type OpenCodeApiEndpointKey = keyof OpenCodeApiEndpointMap;
export type OpenCodeEndpointEvidence =
| 'openapi'
| 'direct_probe'
| 'undocumented_direct_probe'
| 'real_e2e'
| 'missing';
export type OpenCodeApiCapabilitySource =
| 'openapi_doc'
| 'sdk_probe'
| 'direct_probe'
| 'mixed_openapi_direct_probe';
export interface OpenCodeApiCapabilities {
version: string | null;
source: OpenCodeApiCapabilitySource;
endpoints: OpenCodeApiEndpointMap;
requiredForTeamLaunch: {
ready: boolean;
missing: string[];
};
evidence: Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence>;
diagnostics: string[];
}
export interface OpenCodeApiDiscoverySnapshot {
checkedAt: string;
opencodeVersion: string | null;
baseUrlRedacted: string;
capabilities: OpenCodeApiCapabilities;
openApiHash: string | null;
}
export interface OpenCodeApiCapabilityDetectorInput {
baseUrl: string;
fetchImpl?: typeof fetch;
timeoutMs?: number;
}
export interface OpenCodeApiDiscoverySnapshotInput {
baseUrl: string;
checkedAt: string;
capabilities: OpenCodeApiCapabilities;
openApiDocument?: unknown;
}
interface OpenApiDocument {
openapi?: string;
info?: {
version?: unknown;
};
paths?: Record<string, Record<string, unknown>>;
}
interface RequiredOpenCodeEndpoint {
key: OpenCodeApiEndpointKey;
method: 'get' | 'post' | 'delete' | 'patch';
path: RegExp;
label: string;
}
interface DirectSafeProbe {
method: 'GET';
path: string;
accept: 'application/json' | 'text/event-stream';
}
const OPENAPI_SPEC_CANDIDATES = ['/doc', '/doc.json', '/openapi.json'] as const;
export const REQUIRED_OPENCODE_ENDPOINTS: RequiredOpenCodeEndpoint[] = [
{ key: 'health', method: 'get', path: /^\/global\/health\/?$/, label: 'GET /global/health' },
{ key: 'sessionCreate', method: 'post', path: /^\/session\/?$/, label: 'POST /session' },
{
key: 'sessionGet',
method: 'get',
path: /^\/session\/(?:\{[^}]+\}|:[^/]+)\/?$/,
label: 'GET /session/:id',
},
{
key: 'sessionMessageList',
method: 'get',
path: /^\/session\/(?:\{[^}]+\}|:[^/]+)\/message\/?$/,
label: 'GET /session/:id/message',
},
{
key: 'sessionPromptAsync',
method: 'post',
path: /^\/session\/(?:\{[^}]+\}|:[^/]+)\/prompt_async\/?$/,
label: 'POST /session/:id/prompt_async',
},
{
key: 'sessionAbort',
method: 'post',
path: /^\/session\/(?:\{[^}]+\}|:[^/]+)\/abort\/?$/,
label: 'POST /session/:id/abort',
},
{
key: 'sessionStatus',
method: 'get',
path: /^\/session\/status\/?$/,
label: 'GET /session/status',
},
{ key: 'permissionList', method: 'get', path: /^\/permission\/?$/, label: 'GET /permission' },
{
key: 'permissionReply',
method: 'post',
path: /^\/permission\/(?:\{[^}]+\}|:[^/]+)\/reply\/?$/,
label: 'POST /permission/:requestID/reply',
},
{
key: 'permissionLegacySessionRespond',
method: 'post',
path: /^\/session\/(?:\{[^}]+\}|:[^/]+)\/permissions\/(?:\{[^}]+\}|:[^/]+)\/?$/,
label: 'POST /session/:sessionID/permissions/:permissionID',
},
{ key: 'sessionEventStream', method: 'get', path: /^\/event\/?$/, label: 'GET /event' },
{
key: 'globalEventStream',
method: 'get',
path: /^\/global\/event\/?$/,
label: 'GET /global/event',
},
{ key: 'mcpList', method: 'get', path: /^\/mcp\/?$/, label: 'GET /mcp' },
{ key: 'mcpCreate', method: 'post', path: /^\/mcp\/?$/, label: 'POST /mcp' },
{
key: 'experimentalToolIds',
method: 'get',
path: /^\/experimental\/tool\/ids\/?$/,
label: 'GET /experimental/tool/ids',
},
{
key: 'experimentalToolList',
method: 'get',
path: /^\/experimental\/tool\/?$/,
label: 'GET /experimental/tool',
},
];
const DIRECT_SAFE_PROBES: Partial<Record<OpenCodeApiEndpointKey, DirectSafeProbe>> = {
health: { method: 'GET', path: '/global/health', accept: 'application/json' },
sessionStatus: { method: 'GET', path: '/session/status', accept: 'application/json' },
permissionList: { method: 'GET', path: '/permission/', accept: 'application/json' },
sessionEventStream: { method: 'GET', path: '/event', accept: 'text/event-stream' },
globalEventStream: { method: 'GET', path: '/global/event', accept: 'text/event-stream' },
mcpList: { method: 'GET', path: '/mcp', accept: 'application/json' },
experimentalToolIds: {
method: 'GET',
path: '/experimental/tool/ids',
accept: 'application/json',
},
experimentalToolList: {
method: 'GET',
path: '/experimental/tool',
accept: 'application/json',
},
};
export async function detectOpenCodeApiCapabilities(
input: OpenCodeApiCapabilityDetectorInput
): Promise<OpenCodeApiCapabilities> {
const fetchImpl = input.fetchImpl ?? fetch;
const timeoutMs = input.timeoutMs ?? 5_000;
const diagnostics: string[] = [];
const endpoints = createEmptyEndpointMap();
const evidence = createEmptyEvidenceMap();
const openApi = await loadOpenApiDocument({
baseUrl: input.baseUrl,
fetchImpl,
timeoutMs,
diagnostics,
});
if (openApi.document?.paths) {
applyOpenApiEndpointEvidence(openApi.document, endpoints, evidence);
}
await runDirectSafeProbes({
baseUrl: input.baseUrl,
fetchImpl,
timeoutMs,
docAvailable: Boolean(openApi.document),
endpoints,
evidence,
diagnostics,
});
if (!endpoints.permissionReply && !endpoints.permissionLegacySessionRespond) {
diagnostics.push(
'OpenCode permission response endpoint was not proven by OpenAPI; require real permission E2E before production launch'
);
}
const missing = resolveMissingOpenCodeCapabilities(endpoints);
const version =
extractOpenApiVersion(openApi.document) ??
(await probeOpenCodeHealthVersion(input.baseUrl, fetchImpl, timeoutMs, diagnostics));
return {
version,
source: resolveCapabilitySource(openApi.document, evidence),
endpoints,
requiredForTeamLaunch: {
ready: missing.length === 0,
missing,
},
evidence,
diagnostics,
};
}
export function createOpenCodeApiDiscoverySnapshot(
input: OpenCodeApiDiscoverySnapshotInput
): OpenCodeApiDiscoverySnapshot {
return {
checkedAt: input.checkedAt,
opencodeVersion: input.capabilities.version,
baseUrlRedacted: redactUrl(input.baseUrl),
capabilities: input.capabilities,
openApiHash: input.openApiDocument === undefined ? null : stableHash(input.openApiDocument),
};
}
export function applyOpenApiEndpointEvidence(
document: OpenApiDocument,
endpoints: OpenCodeApiEndpointMap,
evidence: Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence>
): void {
for (const [path, methods] of Object.entries(document.paths ?? {})) {
for (const required of REQUIRED_OPENCODE_ENDPOINTS) {
if (required.path.test(path) && Boolean(methods[required.method])) {
endpoints[required.key] = true;
evidence[required.key] = 'openapi';
}
}
}
}
export function resolveMissingOpenCodeCapabilities(endpoints: OpenCodeApiEndpointMap): string[] {
const missing: string[] = [];
for (const endpoint of REQUIRED_OPENCODE_ENDPOINTS) {
if (endpoint.key === 'permissionLegacySessionRespond') {
continue;
}
if (endpoint.key === 'experimentalToolList') {
continue;
}
if (endpoint.key === 'permissionReply') {
if (!endpoints.permissionReply && !endpoints.permissionLegacySessionRespond) {
missing.push('POST permission reply route');
}
continue;
}
if (endpoint.key === 'experimentalToolIds') {
if (!endpoints.experimentalToolIds && !endpoints.experimentalToolList) {
missing.push('GET OpenCode tool availability route');
}
continue;
}
if (!endpoints[endpoint.key]) {
missing.push(endpoint.label);
}
}
return missing;
}
export function createEmptyEndpointMap(): OpenCodeApiEndpointMap {
return {
health: false,
sessionCreate: false,
sessionGet: false,
sessionMessageList: false,
sessionPromptAsync: false,
sessionAbort: false,
sessionStatus: false,
permissionList: false,
permissionReply: false,
permissionLegacySessionRespond: false,
sessionEventStream: false,
globalEventStream: false,
mcpList: false,
mcpCreate: false,
experimentalToolIds: false,
experimentalToolList: false,
};
}
function createEmptyEvidenceMap(): Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence> {
return Object.fromEntries(
(Object.keys(createEmptyEndpointMap()) as OpenCodeApiEndpointKey[]).map((key) => [
key,
'missing',
])
) as Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence>;
}
async function loadOpenApiDocument(input: {
baseUrl: string;
fetchImpl: typeof fetch;
timeoutMs: number;
diagnostics: string[];
}): Promise<{ document: OpenApiDocument | null; raw: string | null }> {
for (const candidate of OPENAPI_SPEC_CANDIDATES) {
try {
const response = await fetchWithTimeout(input.fetchImpl, buildUrl(input.baseUrl, candidate), {
timeoutMs: input.timeoutMs,
requestInit: { headers: { accept: 'application/json' } },
});
const text = await response.text();
if (!response.ok) {
input.diagnostics.push(`OpenCode ${candidate} returned HTTP ${response.status}`);
continue;
}
if (looksLikeHtml(text)) {
input.diagnostics.push(`OpenCode ${candidate} returned HTML, expected OpenAPI JSON`);
continue;
}
const parsed = JSON.parse(text) as OpenApiDocument;
if (parsed.paths && Object.keys(parsed.paths).length > 0) {
return { document: parsed, raw: text };
}
input.diagnostics.push(`OpenCode ${candidate} did not include OpenAPI paths`);
} catch (error) {
input.diagnostics.push(`OpenCode ${candidate} probe failed: ${stringifyError(error)}`);
}
}
return { document: null, raw: null };
}
async function runDirectSafeProbes(input: {
baseUrl: string;
fetchImpl: typeof fetch;
timeoutMs: number;
docAvailable: boolean;
endpoints: OpenCodeApiEndpointMap;
evidence: Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence>;
diagnostics: string[];
}): Promise<void> {
for (const [key, probe] of Object.entries(DIRECT_SAFE_PROBES) as Array<
[OpenCodeApiEndpointKey, DirectSafeProbe]
>) {
if (input.endpoints[key]) {
continue;
}
try {
const response = await fetchWithTimeout(
input.fetchImpl,
buildUrl(input.baseUrl, probe.path),
{
timeoutMs: input.timeoutMs,
requestInit: {
method: probe.method,
headers: { accept: probe.accept },
},
}
);
await cancelResponseBody(response);
if (!response.ok) {
input.diagnostics.push(
`OpenCode direct probe ${probe.path} returned HTTP ${response.status}`
);
continue;
}
input.endpoints[key] = true;
input.evidence[key] = input.docAvailable ? 'undocumented_direct_probe' : 'direct_probe';
} catch (error) {
input.diagnostics.push(
`OpenCode direct probe ${probe.path} failed: ${stringifyError(error)}`
);
}
}
}
async function probeOpenCodeHealthVersion(
baseUrl: string,
fetchImpl: typeof fetch,
timeoutMs: number,
diagnostics: string[]
): Promise<string | null> {
try {
const response = await fetchWithTimeout(fetchImpl, buildUrl(baseUrl, '/global/health'), {
timeoutMs,
requestInit: { headers: { accept: 'application/json' } },
});
const text = await response.text();
if (!response.ok) {
diagnostics.push(`OpenCode health version probe returned HTTP ${response.status}`);
return null;
}
const parsed = JSON.parse(text) as unknown;
return extractHealthVersion(parsed);
} catch (error) {
diagnostics.push(`OpenCode health version probe failed: ${stringifyError(error)}`);
return null;
}
}
function extractOpenApiVersion(document: OpenApiDocument | null): string | null {
return typeof document?.info?.version === 'string' && document.info.version.trim().length > 0
? document.info.version
: null;
}
function extractHealthVersion(value: unknown): string | null {
if (!isRecord(value)) {
return null;
}
if (typeof value.version === 'string' && value.version.trim().length > 0) {
return value.version;
}
if (
isRecord(value.build) &&
typeof value.build.version === 'string' &&
value.build.version.trim().length > 0
) {
return value.build.version;
}
if (
isRecord(value.data) &&
typeof value.data.version === 'string' &&
value.data.version.trim().length > 0
) {
return value.data.version;
}
return null;
}
function resolveCapabilitySource(
document: OpenApiDocument | null,
evidence: Record<OpenCodeApiEndpointKey, OpenCodeEndpointEvidence>
): OpenCodeApiCapabilitySource {
if (!document) {
return 'direct_probe';
}
return Object.values(evidence).some((item) => item === 'undocumented_direct_probe')
? 'mixed_openapi_direct_probe'
: 'openapi_doc';
}
async function fetchWithTimeout(
fetchImpl: typeof fetch,
url: string,
options: {
timeoutMs: number;
requestInit?: RequestInit;
}
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
try {
return await fetchImpl(url, {
...options.requestInit,
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
}
async function cancelResponseBody(response: Response): Promise<void> {
try {
await response.body?.cancel();
} catch {
// Best-effort cleanup for SSE probes after headers are proven.
}
}
function buildUrl(baseUrl: string, path: string): string {
return new URL(path, normalizeBaseUrl(baseUrl)).toString();
}
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
}
function looksLikeHtml(text: string): boolean {
return text.trimStart().startsWith('<');
}
function redactUrl(url: string): string {
try {
const parsed = new URL(url);
if (parsed.username) {
parsed.username = 'redacted';
}
if (parsed.password) {
parsed.password = 'redacted';
}
return parsed.toString();
} catch {
return '<invalid-url>';
}
}
function stableHash(value: unknown): string {
return createHash('sha256').update(stableJsonStringify(value)).digest('hex');
}
function stableJsonStringify(value: unknown): string {
if (value === null || typeof value !== 'object') {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(stableJsonStringify).join(',')}]`;
}
return `{${Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => `${JSON.stringify(key)}:${stableJsonStringify(item)}`)
.join(',')}}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,401 @@
import { createHash } from 'crypto';
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
export interface OpenCodeMcpServerConfig {
type: 'local' | 'remote';
command?: string;
args?: string[];
url?: string;
enabled: boolean;
environment?: Record<string, string>;
timeout?: number;
}
export type OpenCodeBehaviorSourceKind =
| 'global_config'
| 'project_config'
| 'global_plugin_dir'
| 'project_plugin_dir'
| 'project_opencode_dir';
export interface OpenCodeBehaviorSource {
kind: OpenCodeBehaviorSourceKind;
pathHash: string;
exists: boolean;
fingerprint: string | null;
fileCount: number;
}
export interface OpenCodeManagedOverlay {
launchMode: 'project_root_with_inline_overlay';
projectPath: string;
env: {
OPENCODE_CONFIG_CONTENT: string;
OPENCODE_DISABLE_AUTOUPDATE: '1';
};
appMcpServerName: string;
appMcpConfig: OpenCodeMcpServerConfig;
preservedSources: OpenCodeBehaviorSource[];
diagnostics: string[];
}
export interface OpenCodeManagedOverlayBuilderInput {
projectPath: string;
preferredMcpName: string;
appMcpCommand: string;
appMcpArgs: string[];
appMcpEnv: Record<string, string>;
mcpTimeoutMs?: number;
}
export interface OpenCodeBehaviorSourceScannerOptions {
homePath?: string;
maxDirectoryFiles?: number;
}
const FORBIDDEN_MANAGED_OVERLAY_TOP_LEVEL_KEYS = [
'plugin',
'plugins',
'agent',
'command',
'instructions',
'formatter',
'lsp',
'theme',
'keybinds',
'model',
'mode',
'provider',
'tools',
'skills',
] as const;
export class OpenCodeManagedOverlayBuilder {
constructor(
private readonly behaviorSourceScanner = new OpenCodeBehaviorSourceScanner(),
private readonly clock: () => Date = () => new Date()
) {}
async build(input: OpenCodeManagedOverlayBuilderInput): Promise<OpenCodeManagedOverlay> {
const preservedSources = await this.behaviorSourceScanner.scan(input.projectPath);
const existingMcpNames = await this.behaviorSourceScanner.readDeclaredMcpNames(
input.projectPath
);
const appMcpServerName = pickAppOwnedMcpServerName(input.preferredMcpName, existingMcpNames);
const overlayConfig = buildManagedOverlayConfig({
serverName: appMcpServerName,
command: input.appMcpCommand,
args: input.appMcpArgs,
environment: input.appMcpEnv,
timeout: input.mcpTimeoutMs ?? 10_000,
});
assertManagedOverlayDoesNotShadowUserConfig(overlayConfig);
return {
launchMode: 'project_root_with_inline_overlay',
projectPath: input.projectPath,
env: {
OPENCODE_CONFIG_CONTENT: JSON.stringify(overlayConfig),
OPENCODE_DISABLE_AUTOUPDATE: '1',
},
appMcpServerName,
appMcpConfig: overlayConfig.mcp[appMcpServerName],
preservedSources,
diagnostics: buildOverlayDiagnostics({
preferredMcpName: input.preferredMcpName,
appMcpServerName,
existingMcpNames,
preservedSources,
checkedAt: this.clock().toISOString(),
}),
};
}
}
export class OpenCodeBehaviorSourceScanner {
private readonly homePath: string;
private readonly maxDirectoryFiles: number;
constructor(options: OpenCodeBehaviorSourceScannerOptions = {}) {
this.homePath = options.homePath ?? os.homedir();
this.maxDirectoryFiles = options.maxDirectoryFiles ?? 200;
}
async scan(projectPath: string): Promise<OpenCodeBehaviorSource[]> {
const sourceSpecs: Array<{ kind: OpenCodeBehaviorSourceKind; targetPath: string }> = [
{
kind: 'global_config',
targetPath: path.join(this.homePath, '.config/opencode/opencode.json'),
},
{ kind: 'project_config', targetPath: path.join(projectPath, 'opencode.json') },
{ kind: 'project_config', targetPath: path.join(projectPath, 'opencode.jsonc') },
{
kind: 'global_plugin_dir',
targetPath: path.join(this.homePath, '.config/opencode/plugins'),
},
{ kind: 'project_plugin_dir', targetPath: path.join(projectPath, '.opencode/plugins') },
{ kind: 'project_opencode_dir', targetPath: path.join(projectPath, '.opencode') },
];
return Promise.all(sourceSpecs.map((source) => this.fingerprintSource(source)));
}
async readDeclaredMcpNames(projectPath: string): Promise<Set<string>> {
const configPaths = [
path.join(this.homePath, '.config/opencode/opencode.json'),
path.join(projectPath, 'opencode.json'),
path.join(projectPath, 'opencode.jsonc'),
path.join(projectPath, '.opencode/opencode.json'),
path.join(projectPath, '.opencode/opencode.jsonc'),
];
const names = new Set<string>();
for (const configPath of configPaths) {
const config = await this.readConfig(configPath);
const mcp = asRecord(config?.mcp);
for (const name of Object.keys(mcp ?? {})) {
names.add(name);
}
}
return names;
}
private async fingerprintSource(input: {
kind: OpenCodeBehaviorSourceKind;
targetPath: string;
}): Promise<OpenCodeBehaviorSource> {
const pathHash = hashText(input.targetPath);
let stat;
try {
stat = await fs.stat(input.targetPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return {
kind: input.kind,
pathHash,
exists: false,
fingerprint: null,
fileCount: 0,
};
}
throw error;
}
if (stat.isFile()) {
const content = await fs.readFile(input.targetPath);
return {
kind: input.kind,
pathHash,
exists: true,
fingerprint: hashText(`${stat.size}:${stat.mtimeMs}:${hashBuffer(content)}`),
fileCount: 1,
};
}
if (stat.isDirectory()) {
const entries = await this.listDirectoryFiles(input.targetPath);
return {
kind: input.kind,
pathHash,
exists: true,
fingerprint: hashJson(
entries.map((entry) => ({
relativePath: entry.relativePath,
size: entry.size,
mtimeMs: entry.mtimeMs,
contentHash: entry.contentHash,
}))
),
fileCount: entries.length,
};
}
return {
kind: input.kind,
pathHash,
exists: true,
fingerprint: hashText(`${stat.size}:${stat.mtimeMs}:unsupported`),
fileCount: 0,
};
}
private async listDirectoryFiles(rootPath: string): Promise<
Array<{
relativePath: string;
size: number;
mtimeMs: number;
contentHash: string;
}>
> {
const results: Array<{
relativePath: string;
size: number;
mtimeMs: number;
contentHash: string;
}> = [];
const visit = async (directoryPath: string): Promise<void> => {
if (results.length >= this.maxDirectoryFiles) {
return;
}
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
for (const entry of entries) {
if (results.length >= this.maxDirectoryFiles) {
return;
}
const absolutePath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
await visit(absolutePath);
continue;
}
if (!entry.isFile()) {
continue;
}
const stat = await fs.stat(absolutePath);
const content = await fs.readFile(absolutePath);
results.push({
relativePath: path.relative(rootPath, absolutePath),
size: stat.size,
mtimeMs: stat.mtimeMs,
contentHash: hashBuffer(content),
});
}
};
await visit(rootPath);
return results.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
}
private async readConfig(configPath: string): Promise<Record<string, unknown> | null> {
let text: string;
try {
text = await fs.readFile(configPath, 'utf8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw error;
}
try {
const parsed = JSON.parse(stripJsonComments(text)) as unknown;
return asRecord(parsed);
} catch {
return null;
}
}
}
export function buildManagedOverlayConfig(input: {
serverName: string;
command: string;
args: string[];
environment: Record<string, string>;
timeout: number;
}): { mcp: Record<string, OpenCodeMcpServerConfig> } {
return {
mcp: {
[input.serverName]: {
type: 'local',
command: input.command,
args: input.args,
enabled: true,
environment: input.environment,
timeout: input.timeout,
},
},
};
}
export function assertManagedOverlayDoesNotShadowUserConfig(config: Record<string, unknown>): void {
const usedForbiddenKeys = FORBIDDEN_MANAGED_OVERLAY_TOP_LEVEL_KEYS.filter((key) => key in config);
if (usedForbiddenKeys.length > 0) {
throw new Error(
`Managed OpenCode overlay must not set user behavior keys: ${usedForbiddenKeys.join(', ')}`
);
}
}
export function pickAppOwnedMcpServerName(preferred: string, existingNames: Set<string>): string {
if (!existingNames.has(preferred)) {
return preferred;
}
let index = 1;
while (existingNames.has(`${preferred}-runtime-${index}`)) {
index += 1;
}
return `${preferred}-runtime-${index}`;
}
function buildOverlayDiagnostics(input: {
preferredMcpName: string;
appMcpServerName: string;
existingMcpNames: Set<string>;
preservedSources: OpenCodeBehaviorSource[];
checkedAt: string;
}): string[] {
const diagnostics = [
`OpenCode managed overlay checked at ${input.checkedAt}`,
`OpenCode preserved behavior sources: ${input.preservedSources.filter((source) => source.exists).length}`,
];
if (input.appMcpServerName !== input.preferredMcpName) {
diagnostics.push(
`User OpenCode config already declares MCP server "${input.preferredMcpName}"; managed runtime will use "${input.appMcpServerName}"`
);
}
if (input.existingMcpNames.size > 0) {
diagnostics.push(
`OpenCode existing MCP server names observed: ${[...input.existingMcpNames].sort().join(', ')}`
);
}
return diagnostics;
}
function stripJsonComments(text: string): string {
return text
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, (_match, prefix: string) => prefix);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function hashJson(value: unknown): string {
return hashText(stableJsonStringify(value));
}
function hashText(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
function hashBuffer(value: Buffer): string {
return createHash('sha256').update(value).digest('hex');
}
function stableJsonStringify(value: unknown): string {
if (value === null || typeof value !== 'object') {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(stableJsonStringify).join(',')}]`;
}
return `{${Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => `${JSON.stringify(key)}:${stableJsonStringify(item)}`)
.join(',')}}`;
}

View file

@ -0,0 +1,482 @@
import { stableHash, stableJsonStringify } from '../bridge/OpenCodeBridgeCommandContract';
import { VersionedJsonStore, VersionedJsonStoreError } from '../store/VersionedJsonStore';
export const RUNTIME_DELIVERY_JOURNAL_SCHEMA_VERSION = 1;
export type RuntimeDeliveryJournalStatus =
| 'pending'
| 'committed'
| 'failed_retryable'
| 'failed_terminal';
export type RuntimeDeliveryDestinationRef =
| { kind: 'user_sent_messages'; teamName: string }
| { kind: 'member_inbox'; teamName: string; memberName: string }
| {
kind: 'cross_team_outbox';
fromTeamName: string;
toTeamName: string;
toMemberName: string;
};
export type RuntimeDeliveryLocation =
| { kind: 'user_sent_messages'; teamName: string; messageId: string }
| { kind: 'member_inbox'; teamName: string; memberName: string; messageId: string }
| {
kind: 'cross_team_outbox';
fromTeamName: string;
toTeamName: string;
toMemberName: string;
messageId: string;
};
export interface RuntimeDeliveryJournalRecord {
idempotencyKey: string;
runId: string;
teamName: string;
fromMemberName: string;
providerId: 'opencode';
runtimeSessionId: string;
payloadHash: string;
destination: RuntimeDeliveryDestinationRef;
destinationMessageId: string;
committedLocation: RuntimeDeliveryLocation | null;
status: RuntimeDeliveryJournalStatus;
attempts: number;
createdAt: string;
updatedAt: string;
committedAt: string | null;
lastError: string | null;
}
export interface RuntimeDeliveryJournalBeginInput {
idempotencyKey: string;
payloadHash: string;
runId: string;
teamName: string;
fromMemberName: string;
providerId: 'opencode';
runtimeSessionId: string;
destination: RuntimeDeliveryDestinationRef;
destinationMessageId: string;
now: string;
}
export type RuntimeDeliveryJournalBeginResult =
| { state: 'new'; record: RuntimeDeliveryJournalRecord }
| { state: 'already_committed'; record: RuntimeDeliveryJournalRecord }
| { state: 'resume_pending'; record: RuntimeDeliveryJournalRecord }
| { state: 'payload_conflict'; record: RuntimeDeliveryJournalRecord };
export class RuntimeDeliveryJournalStore {
constructor(private readonly store: VersionedJsonStore<RuntimeDeliveryJournalRecord[]>) {}
async begin(input: RuntimeDeliveryJournalBeginInput): Promise<RuntimeDeliveryJournalBeginResult> {
let result: RuntimeDeliveryJournalBeginResult | null = null;
await this.store.updateLocked((records) => {
const existing = records.find((record) => record.idempotencyKey === input.idempotencyKey);
if (existing) {
if (existing.payloadHash !== input.payloadHash) {
result = { state: 'payload_conflict', record: existing };
return records;
}
if (existing.status === 'committed') {
result = { state: 'already_committed', record: existing };
return records;
}
const resumed = {
...existing,
attempts: existing.attempts + 1,
status: existing.status === 'failed_terminal' ? existing.status : 'pending',
updatedAt: input.now,
} satisfies RuntimeDeliveryJournalRecord;
result = { state: 'resume_pending', record: resumed };
return records.map((record) =>
record.idempotencyKey === input.idempotencyKey ? resumed : record
);
}
const created: RuntimeDeliveryJournalRecord = {
idempotencyKey: input.idempotencyKey,
runId: input.runId,
teamName: input.teamName,
fromMemberName: input.fromMemberName,
providerId: input.providerId,
runtimeSessionId: input.runtimeSessionId,
payloadHash: input.payloadHash,
destination: input.destination,
destinationMessageId: input.destinationMessageId,
committedLocation: null,
status: 'pending',
attempts: 1,
createdAt: input.now,
updatedAt: input.now,
committedAt: null,
lastError: null,
};
result = { state: 'new', record: created };
return [...records, created];
});
if (!result) {
throw new Error('Runtime delivery journal begin failed');
}
return result;
}
async markCommitted(input: {
idempotencyKey: string;
location: RuntimeDeliveryLocation;
committedAt: string;
}): Promise<void> {
await this.updateExisting(input.idempotencyKey, (record) => ({
...record,
committedLocation: input.location,
status: 'committed',
updatedAt: input.committedAt,
committedAt: input.committedAt,
lastError: null,
}));
}
async markFailed(input: {
idempotencyKey: string;
status: 'failed_retryable' | 'failed_terminal';
error: string;
updatedAt: string;
}): Promise<void> {
await this.updateExisting(input.idempotencyKey, (record) => ({
...record,
status: input.status,
updatedAt: input.updatedAt,
lastError: input.error,
}));
}
async get(idempotencyKey: string): Promise<RuntimeDeliveryJournalRecord | null> {
const records = await this.readRequired();
return records.find((record) => record.idempotencyKey === idempotencyKey) ?? null;
}
async listRecoverable(teamName: string): Promise<RuntimeDeliveryJournalRecord[]> {
const records = await this.readRequired();
return records.filter(
(record) =>
record.teamName === teamName &&
(record.status === 'pending' || record.status === 'failed_retryable')
);
}
async findCommittedByRuntimeSession(input: {
teamName: string;
runId: string;
runtimeSessionId: string;
}): Promise<Map<string, RuntimeDeliveryJournalRecord>> {
const records = await this.readRequired();
return new Map(
records
.filter(
(record) =>
record.teamName === input.teamName &&
record.runId === input.runId &&
record.runtimeSessionId === input.runtimeSessionId &&
record.status === 'committed'
)
.map((record) => [record.idempotencyKey, record])
);
}
async list(): Promise<RuntimeDeliveryJournalRecord[]> {
return this.readRequired();
}
private async updateExisting(
idempotencyKey: string,
updater: (record: RuntimeDeliveryJournalRecord) => RuntimeDeliveryJournalRecord
): Promise<void> {
let found = false;
await this.store.updateLocked((records) =>
records.map((record) => {
if (record.idempotencyKey !== idempotencyKey) {
return record;
}
found = true;
return updater(record);
})
);
if (!found) {
throw new Error(`Runtime delivery journal record not found: ${idempotencyKey}`);
}
}
private async readRequired(): Promise<RuntimeDeliveryJournalRecord[]> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
return result.data;
}
}
export function createRuntimeDeliveryJournalStore(options: {
filePath: string;
clock?: () => Date;
}): RuntimeDeliveryJournalStore {
const clock = options.clock ?? (() => new Date());
return new RuntimeDeliveryJournalStore(
new VersionedJsonStore<RuntimeDeliveryJournalRecord[]>({
filePath: options.filePath,
schemaVersion: RUNTIME_DELIVERY_JOURNAL_SCHEMA_VERSION,
defaultData: () => [],
validate: validateRuntimeDeliveryJournalRecords,
clock,
})
);
}
export function validateRuntimeDeliveryJournalRecords(
value: unknown
): RuntimeDeliveryJournalRecord[] {
if (!Array.isArray(value)) {
throw new Error('Runtime delivery journal must be an array');
}
const seen = new Set<string>();
return value.map((record, index) => {
if (!isRuntimeDeliveryJournalRecord(record)) {
throw new Error(`Invalid runtime delivery journal record at index ${index}`);
}
if (seen.has(record.idempotencyKey)) {
throw new Error(`Duplicate runtime delivery idempotency key: ${record.idempotencyKey}`);
}
seen.add(record.idempotencyKey);
return record;
});
}
export function hashRuntimeDeliveryEnvelope(envelope: RuntimeDeliveryEnvelope): string {
return `sha256:${stableHash({
providerId: envelope.providerId,
runId: envelope.runId,
teamName: envelope.teamName,
fromMemberName: envelope.fromMemberName,
runtimeSessionId: envelope.runtimeSessionId,
to: envelope.to,
text: envelope.text,
summary: envelope.summary ?? null,
taskRefs: envelope.taskRefs ?? [],
createdAt: envelope.createdAt,
})}`;
}
export function buildRuntimeDestinationMessageId(envelope: RuntimeDeliveryEnvelope): string {
return `runtime-delivery-${stableHash({
idempotencyKey: envelope.idempotencyKey,
runId: envelope.runId,
teamName: envelope.teamName,
}).slice(0, 32)}`;
}
export type RuntimeDeliveryTarget =
| 'user'
| { memberName: string }
| { teamName: string; memberName: string };
export interface RuntimeDeliveryEnvelope {
idempotencyKey: string;
runId: string;
teamName: string;
fromMemberName: string;
providerId: 'opencode';
runtimeSessionId: string;
to: RuntimeDeliveryTarget;
text: string;
createdAt: string;
summary?: string | null;
taskRefs?: string[];
}
export function normalizeRuntimeDeliveryEnvelope(value: unknown): RuntimeDeliveryEnvelope {
if (!isRecord(value)) {
throw new Error('Runtime delivery envelope must be an object');
}
const envelope: RuntimeDeliveryEnvelope = {
idempotencyKey: requireNonEmptyString(value.idempotencyKey, 'idempotencyKey'),
runId: requireNonEmptyString(value.runId, 'runId'),
teamName: requireNonEmptyString(value.teamName, 'teamName'),
fromMemberName: requireNonEmptyString(value.fromMemberName, 'fromMemberName'),
providerId: value.providerId === 'opencode' ? 'opencode' : fail('providerId must be opencode'),
runtimeSessionId: requireNonEmptyString(value.runtimeSessionId, 'runtimeSessionId'),
to: normalizeRuntimeDeliveryTarget(value.to),
text: requireNonEmptyString(value.text, 'text'),
createdAt: requireNonEmptyString(value.createdAt, 'createdAt'),
summary: value.summary === undefined || value.summary === null ? null : String(value.summary),
taskRefs: Array.isArray(value.taskRefs)
? value.taskRefs.filter((item): item is string => typeof item === 'string')
: [],
};
return envelope;
}
export function resolveRuntimeDeliveryDestination(
envelope: RuntimeDeliveryEnvelope
): RuntimeDeliveryDestinationRef {
if (envelope.to === 'user') {
return { kind: 'user_sent_messages', teamName: envelope.teamName };
}
if ('memberName' in envelope.to && !('teamName' in envelope.to)) {
return {
kind: 'member_inbox',
teamName: envelope.teamName,
memberName: envelope.to.memberName,
};
}
return {
kind: 'cross_team_outbox',
fromTeamName: envelope.teamName,
toTeamName: envelope.to.teamName,
toMemberName: envelope.to.memberName,
};
}
export function buildLocationFromJournal(
record: RuntimeDeliveryJournalRecord
): RuntimeDeliveryLocation {
if (record.committedLocation) {
return record.committedLocation;
}
switch (record.destination.kind) {
case 'user_sent_messages':
return {
kind: 'user_sent_messages',
teamName: record.destination.teamName,
messageId: record.destinationMessageId,
};
case 'member_inbox':
return {
kind: 'member_inbox',
teamName: record.destination.teamName,
memberName: record.destination.memberName,
messageId: record.destinationMessageId,
};
case 'cross_team_outbox':
return {
kind: 'cross_team_outbox',
fromTeamName: record.destination.fromTeamName,
toTeamName: record.destination.toTeamName,
toMemberName: record.destination.toMemberName,
messageId: record.destinationMessageId,
};
}
}
export function runtimeDeliveryEnvelopeStableJson(envelope: RuntimeDeliveryEnvelope): string {
return stableJsonStringify(envelope);
}
function normalizeRuntimeDeliveryTarget(value: unknown): RuntimeDeliveryTarget {
if (value === 'user') {
return 'user';
}
if (!isRecord(value)) {
throw new Error('Runtime delivery target must be user or object');
}
const memberName = requireNonEmptyString(value.memberName, 'to.memberName');
if (typeof value.teamName === 'string' && value.teamName.trim()) {
return { teamName: value.teamName, memberName };
}
return { memberName };
}
function isRuntimeDeliveryJournalRecord(value: unknown): value is RuntimeDeliveryJournalRecord {
return (
isRecord(value) &&
isNonEmptyString(value.idempotencyKey) &&
isNonEmptyString(value.runId) &&
isNonEmptyString(value.teamName) &&
isNonEmptyString(value.fromMemberName) &&
value.providerId === 'opencode' &&
isNonEmptyString(value.runtimeSessionId) &&
isNonEmptyString(value.payloadHash) &&
isRuntimeDeliveryDestinationRef(value.destination) &&
isNonEmptyString(value.destinationMessageId) &&
(value.committedLocation === null || isRuntimeDeliveryLocation(value.committedLocation)) &&
isRuntimeDeliveryJournalStatus(value.status) &&
Number.isInteger(value.attempts) &&
(value.attempts as number) >= 1 &&
isNonEmptyString(value.createdAt) &&
isNonEmptyString(value.updatedAt) &&
(value.committedAt === null || isNonEmptyString(value.committedAt)) &&
(value.lastError === null || typeof value.lastError === 'string')
);
}
function isRuntimeDeliveryJournalStatus(value: unknown): value is RuntimeDeliveryJournalStatus {
return (
value === 'pending' ||
value === 'committed' ||
value === 'failed_retryable' ||
value === 'failed_terminal'
);
}
function isRuntimeDeliveryDestinationRef(value: unknown): value is RuntimeDeliveryDestinationRef {
if (!isRecord(value)) {
return false;
}
if (value.kind === 'user_sent_messages') {
return isNonEmptyString(value.teamName);
}
if (value.kind === 'member_inbox') {
return isNonEmptyString(value.teamName) && isNonEmptyString(value.memberName);
}
return (
value.kind === 'cross_team_outbox' &&
isNonEmptyString(value.fromTeamName) &&
isNonEmptyString(value.toTeamName) &&
isNonEmptyString(value.toMemberName)
);
}
function isRuntimeDeliveryLocation(value: unknown): value is RuntimeDeliveryLocation {
if (!isRecord(value) || !isNonEmptyString(value.messageId)) {
return false;
}
if (value.kind === 'user_sent_messages') {
return isNonEmptyString(value.teamName);
}
if (value.kind === 'member_inbox') {
return isNonEmptyString(value.teamName) && isNonEmptyString(value.memberName);
}
return (
value.kind === 'cross_team_outbox' &&
isNonEmptyString(value.fromTeamName) &&
isNonEmptyString(value.toTeamName) &&
isNonEmptyString(value.toMemberName)
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function requireNonEmptyString(value: unknown, field: string): string {
if (!isNonEmptyString(value)) {
throw new Error(`Runtime delivery envelope missing ${field}`);
}
return value;
}
function fail(message: string): never {
throw new Error(message);
}

View file

@ -0,0 +1,306 @@
import {
buildLocationFromJournal,
buildRuntimeDestinationMessageId,
hashRuntimeDeliveryEnvelope,
normalizeRuntimeDeliveryEnvelope,
resolveRuntimeDeliveryDestination,
type RuntimeDeliveryDestinationRef,
type RuntimeDeliveryEnvelope,
type RuntimeDeliveryJournalRecord,
RuntimeDeliveryJournalStore,
type RuntimeDeliveryLocation,
} from './RuntimeDeliveryJournal';
export interface RuntimeDeliveryVerifyResult {
found: boolean;
location: RuntimeDeliveryLocation | null;
diagnostics: string[];
}
export interface RuntimeDeliveryDestinationPort {
readonly kind: RuntimeDeliveryDestinationRef['kind'];
write(input: {
envelope: RuntimeDeliveryEnvelope;
destinationMessageId: string;
}): Promise<RuntimeDeliveryLocation>;
verify(input: {
destination: RuntimeDeliveryDestinationRef;
destinationMessageId: string;
}): Promise<RuntimeDeliveryVerifyResult>;
buildChangeEvent(input: {
teamName: string;
location: RuntimeDeliveryLocation;
}): RuntimeDeliveryTeamChangeEvent | null;
}
export interface RuntimeDeliveryTeamChangeEvent {
type: string;
teamName: string;
data?: Record<string, unknown>;
}
export interface RuntimeDeliveryRunStateReader {
getCurrentRunId(teamName: string): Promise<string | null>;
}
export interface RuntimeDeliveryDiagnosticsSink {
append(event: RuntimeDeliveryDiagnosticEvent): Promise<void>;
}
export interface RuntimeDeliveryDiagnosticEvent {
type:
| 'runtime_delivery_conflict'
| 'runtime_delivery_failed'
| 'runtime_delivery_recovery_needed';
providerId: 'opencode';
teamName: string;
runId: string;
severity: 'warning' | 'error';
message: string;
data?: Record<string, unknown>;
createdAt: string;
}
export interface RuntimeDeliveryTeamChangeEmitter {
emit(event: RuntimeDeliveryTeamChangeEvent): void;
}
export type RuntimeDeliveryAck =
| {
ok: true;
delivered: boolean;
reason: null | 'duplicate' | 'duplicate_destination_found';
idempotencyKey: string;
location: RuntimeDeliveryLocation;
}
| {
ok: false;
delivered: false;
reason: 'stale_run' | 'idempotency_conflict';
idempotencyKey: string;
};
export class RuntimeDeliveryDestinationRegistry {
private readonly ports = new Map<
RuntimeDeliveryDestinationRef['kind'],
RuntimeDeliveryDestinationPort
>();
constructor(ports: RuntimeDeliveryDestinationPort[]) {
for (const port of ports) {
if (this.ports.has(port.kind)) {
throw new Error(`Duplicate runtime delivery destination port: ${port.kind}`);
}
this.ports.set(port.kind, port);
}
}
get(kind: RuntimeDeliveryDestinationRef['kind']): RuntimeDeliveryDestinationPort {
const port = this.ports.get(kind);
if (!port) {
throw new Error(`Runtime delivery destination port not registered: ${kind}`);
}
return port;
}
}
export class RuntimeDeliveryService {
constructor(
private readonly runState: RuntimeDeliveryRunStateReader,
private readonly journal: RuntimeDeliveryJournalStore,
private readonly destinations: RuntimeDeliveryDestinationRegistry,
private readonly diagnostics: RuntimeDeliveryDiagnosticsSink,
private readonly teamChangeEmitter: RuntimeDeliveryTeamChangeEmitter,
private readonly clock: () => Date = () => new Date()
) {}
async deliver(raw: unknown): Promise<RuntimeDeliveryAck> {
const envelope = normalizeRuntimeDeliveryEnvelope(raw);
const now = this.clock().toISOString();
const currentRunId = await this.runState.getCurrentRunId(envelope.teamName);
if (currentRunId !== envelope.runId) {
return {
ok: false,
delivered: false,
reason: 'stale_run',
idempotencyKey: envelope.idempotencyKey,
};
}
const destination = resolveRuntimeDeliveryDestination(envelope);
const destinationMessageId = buildRuntimeDestinationMessageId(envelope);
const payloadHash = hashRuntimeDeliveryEnvelope(envelope);
const begin = await this.journal.begin({
idempotencyKey: envelope.idempotencyKey,
payloadHash,
runId: envelope.runId,
teamName: envelope.teamName,
fromMemberName: envelope.fromMemberName,
providerId: envelope.providerId,
runtimeSessionId: envelope.runtimeSessionId,
destination,
destinationMessageId,
now,
});
if (begin.state === 'payload_conflict') {
await this.diagnostics.append({
type: 'runtime_delivery_conflict',
providerId: 'opencode',
teamName: envelope.teamName,
runId: envelope.runId,
severity: 'error',
message: 'Runtime delivery idempotency key was reused with a different payload',
data: {
idempotencyKey: envelope.idempotencyKey,
existingPayloadHash: begin.record.payloadHash,
newPayloadHash: payloadHash,
},
createdAt: now,
});
return {
ok: false,
delivered: false,
reason: 'idempotency_conflict',
idempotencyKey: envelope.idempotencyKey,
};
}
if (begin.state === 'already_committed') {
return {
ok: true,
delivered: false,
reason: 'duplicate',
idempotencyKey: envelope.idempotencyKey,
location: buildLocationFromJournal(begin.record),
};
}
const port = this.destinations.get(destination.kind);
const preExisting = await port.verify({ destination, destinationMessageId });
if (preExisting.found && preExisting.location) {
await this.journal.markCommitted({
idempotencyKey: envelope.idempotencyKey,
location: preExisting.location,
committedAt: now,
});
return {
ok: true,
delivered: false,
reason: 'duplicate_destination_found',
idempotencyKey: envelope.idempotencyKey,
location: preExisting.location,
};
}
try {
const location = await port.write({ envelope, destinationMessageId });
const verified = await port.verify({ destination, destinationMessageId });
if (!verified.found) {
throw new Error(
`Delivery destination write was not verifiable for ${destinationMessageId}`
);
}
const committedLocation = verified.location ?? location;
await this.journal.markCommitted({
idempotencyKey: envelope.idempotencyKey,
location: committedLocation,
committedAt: this.clock().toISOString(),
});
const change = port.buildChangeEvent({
teamName: envelope.teamName,
location: committedLocation,
});
if (change) {
this.teamChangeEmitter.emit(change);
}
return {
ok: true,
delivered: true,
reason: null,
idempotencyKey: envelope.idempotencyKey,
location: committedLocation,
};
} catch (error) {
await this.journal.markFailed({
idempotencyKey: envelope.idempotencyKey,
status: 'failed_retryable',
error: stringifyError(error),
updatedAt: this.clock().toISOString(),
});
await this.diagnostics.append({
type: 'runtime_delivery_failed',
providerId: 'opencode',
teamName: envelope.teamName,
runId: envelope.runId,
severity: 'warning',
message: 'Runtime delivery failed and remains retryable',
data: {
idempotencyKey: envelope.idempotencyKey,
destination,
error: stringifyError(error),
},
createdAt: this.clock().toISOString(),
});
throw error;
}
}
}
export class RuntimeDeliveryReconciler {
constructor(
private readonly journal: RuntimeDeliveryJournalStore,
private readonly destinations: RuntimeDeliveryDestinationRegistry,
private readonly diagnostics: RuntimeDeliveryDiagnosticsSink,
private readonly clock: () => Date = () => new Date()
) {}
async reconcileTeam(teamName: string): Promise<void> {
const records = await this.journal.listRecoverable(teamName);
for (const record of records) {
await this.reconcileRecord(record);
}
}
private async reconcileRecord(record: RuntimeDeliveryJournalRecord): Promise<void> {
const port = this.destinations.get(record.destination.kind);
const verified = await port.verify({
destination: record.destination,
destinationMessageId: record.destinationMessageId,
});
if (verified.found && verified.location) {
await this.journal.markCommitted({
idempotencyKey: record.idempotencyKey,
location: verified.location,
committedAt: this.clock().toISOString(),
});
return;
}
await this.diagnostics.append({
type: 'runtime_delivery_recovery_needed',
providerId: 'opencode',
teamName: record.teamName,
runId: record.runId,
severity: 'warning',
message: `Runtime delivery ${record.idempotencyKey} is pending and destination write is not visible`,
data: {
destination: record.destination,
attempts: record.attempts,
lastError: record.lastError,
},
createdAt: this.clock().toISOString(),
});
}
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,527 @@
export const OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION = 1;
export const OPENCODE_PRODUCTION_E2E_EVIDENCE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
export const OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS = [
'required_tools_proven',
'delivery_ready',
'member_ready',
'run_ready',
] as const;
export const OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS = [
'app_mcp_tools_visible',
'state_changing_launch_completed',
'session_records_persisted',
'bootstrap_confirmed_alive',
'canonical_log_projection_observed',
'reconcile_completed',
'stop_completed',
'stale_run_rejected',
] as const;
export type OpenCodeProductionE2ERequiredSignal =
(typeof OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS)[number];
export interface OpenCodeProductionE2ECheckpointEvidence {
name: string;
observedAt: string;
}
export interface OpenCodeProductionE2ESessionEvidence {
memberName: string;
sessionId: string;
launchState: 'confirmed_alive';
}
export interface OpenCodeProductionE2EEvidence {
schemaVersion: typeof OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION;
evidenceId: string;
createdAt: string;
expiresAt: string;
version: string;
passed: boolean;
artifactPath: string | null;
binaryFingerprint: string;
capabilitySnapshotId: string;
selectedModel: string;
projectPathFingerprint: string | null;
requiredSignals: Record<OpenCodeProductionE2ERequiredSignal, boolean>;
mcpTools: {
requiredTools: string[];
observedTools: string[];
};
launch: {
runId: string;
teamId: string;
teamLaunchState: 'ready';
memberCount: number;
sessions: OpenCodeProductionE2ESessionEvidence[];
durableCheckpoints: OpenCodeProductionE2ECheckpointEvidence[];
};
reconcile: {
runId: string;
teamLaunchState: 'ready';
memberCount: number;
};
stop: {
runId: string;
stopped: true;
stoppedSessionIds: string[];
};
logProjection: {
observed: true;
projectedMessageCount: number;
};
diagnostics?: string[];
}
export interface OpenCodeProductionE2EGateExpectation {
opencodeVersion: string | null;
binaryFingerprint: string | null;
capabilitySnapshotId: string | null;
selectedModel: string | null;
requiredMcpTools?: string[];
}
export interface OpenCodeProductionE2EGateResult {
ok: boolean;
diagnostics: string[];
}
export function validateOpenCodeProductionE2EEvidence(
value: unknown
): OpenCodeProductionE2EEvidence {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence must be an object');
}
if (record.schemaVersion !== OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION) {
throw new Error('OpenCode production E2E evidence has unsupported schemaVersion');
}
const evidence: OpenCodeProductionE2EEvidence = {
schemaVersion: OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION,
evidenceId: requireString(record.evidenceId, 'evidenceId'),
createdAt: requireIsoDate(record.createdAt, 'createdAt'),
expiresAt: requireIsoDate(record.expiresAt, 'expiresAt'),
version: requireString(record.version, 'version'),
passed: requireBoolean(record.passed, 'passed'),
artifactPath: optionalString(record.artifactPath, 'artifactPath'),
binaryFingerprint: requireString(record.binaryFingerprint, 'binaryFingerprint'),
capabilitySnapshotId: requireString(record.capabilitySnapshotId, 'capabilitySnapshotId'),
selectedModel: requireString(record.selectedModel, 'selectedModel'),
projectPathFingerprint: optionalString(record.projectPathFingerprint, 'projectPathFingerprint'),
requiredSignals: normalizeRequiredSignals(record.requiredSignals),
mcpTools: normalizeMcpTools(record.mcpTools),
launch: normalizeLaunch(record.launch),
reconcile: normalizeReconcile(record.reconcile),
stop: normalizeStop(record.stop),
logProjection: normalizeLogProjection(record.logProjection),
diagnostics: optionalStringArray(record.diagnostics, 'diagnostics'),
};
return evidence;
}
export function validateNullableOpenCodeProductionE2EEvidence(
value: unknown
): OpenCodeProductionE2EEvidence | null {
if (value === null) {
return null;
}
return validateOpenCodeProductionE2EEvidence(value);
}
export function assertOpenCodeProductionE2EEvidenceBasics(input: {
evidence: OpenCodeProductionE2EEvidence | null;
testedVersion: string;
now?: Date;
artifactPath?: string | null;
}): OpenCodeProductionE2EGateResult {
const diagnostics: string[] = [];
const now = input.now ?? new Date();
const artifactPath = input.artifactPath ?? input.evidence?.artifactPath ?? null;
if (!input.evidence) {
return {
ok: false,
diagnostics: [
'OpenCode version is capability-compatible but production E2E evidence is missing',
],
};
}
diagnostics.push(...collectArtifactShapeDiagnostics(input.evidence, now, artifactPath));
if (input.evidence.version !== input.testedVersion) {
diagnostics.push(
`OpenCode production E2E evidence version ${input.evidence.version} does not match tested version ${input.testedVersion}`
);
}
return {
ok: diagnostics.length === 0,
diagnostics,
};
}
export function assertOpenCodeProductionE2EArtifactGate(input: {
evidence: OpenCodeProductionE2EEvidence | null;
expected: OpenCodeProductionE2EGateExpectation;
now?: Date;
artifactPath?: string | null;
}): OpenCodeProductionE2EGateResult {
const diagnostics: string[] = [];
const now = input.now ?? new Date();
const artifactPath = input.artifactPath ?? input.evidence?.artifactPath ?? null;
if (!input.evidence) {
return {
ok: false,
diagnostics: [
'OpenCode production launch requires a current production E2E evidence artifact',
],
};
}
diagnostics.push(...collectArtifactShapeDiagnostics(input.evidence, now, artifactPath));
diagnostics.push(...collectExpectedRuntimeDiagnostics(input.evidence, input.expected));
return {
ok: diagnostics.length === 0,
diagnostics,
};
}
function collectArtifactShapeDiagnostics(
evidence: OpenCodeProductionE2EEvidence,
now: Date,
artifactPath: string | null
): string[] {
const diagnostics: string[] = [];
const createdAtMs = Date.parse(evidence.createdAt);
const expiresAtMs = Date.parse(evidence.expiresAt);
if (!evidence.passed) {
diagnostics.push('OpenCode production E2E evidence did not pass');
}
if (!artifactPath) {
diagnostics.push('OpenCode production E2E evidence artifact path is missing');
}
if (!Number.isFinite(createdAtMs)) {
diagnostics.push('OpenCode production E2E evidence createdAt is invalid');
} else if (now.getTime() - createdAtMs > OPENCODE_PRODUCTION_E2E_EVIDENCE_MAX_AGE_MS) {
diagnostics.push('OpenCode production E2E evidence is older than the maximum allowed age');
}
if (!Number.isFinite(expiresAtMs)) {
diagnostics.push('OpenCode production E2E evidence expiresAt is invalid');
} else if (expiresAtMs <= now.getTime()) {
diagnostics.push('OpenCode production E2E evidence is expired');
}
const missingSignals = OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.filter(
(signal) => evidence.requiredSignals[signal] !== true
);
if (missingSignals.length > 0) {
diagnostics.push(
`OpenCode production E2E evidence is missing signals: ${missingSignals.join(', ')}`
);
}
const checkpointNames = new Set(
evidence.launch.durableCheckpoints.map((checkpoint) => checkpoint.name)
);
const missingCheckpoints = OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.filter(
(checkpoint) => !checkpointNames.has(checkpoint)
);
if (missingCheckpoints.length > 0) {
diagnostics.push(
`OpenCode production E2E evidence is missing durable checkpoints: ${missingCheckpoints.join(', ')}`
);
}
if (
evidence.launch.memberCount <= 0 ||
evidence.launch.sessions.length !== evidence.launch.memberCount
) {
diagnostics.push(
'OpenCode production E2E evidence must include confirmed session evidence for every member'
);
}
if (evidence.reconcile.runId !== evidence.launch.runId) {
diagnostics.push(
'OpenCode production E2E reconcile evidence runId does not match launch runId'
);
}
if (evidence.reconcile.memberCount !== evidence.launch.memberCount) {
diagnostics.push(
'OpenCode production E2E reconcile member count does not match launch member count'
);
}
if (evidence.stop.runId !== evidence.launch.runId) {
diagnostics.push('OpenCode production E2E stop evidence runId does not match launch runId');
}
if (evidence.stop.stoppedSessionIds.length < evidence.launch.sessions.length) {
diagnostics.push(
'OpenCode production E2E evidence does not prove every launched session was stopped'
);
}
if (evidence.logProjection.projectedMessageCount <= 0) {
diagnostics.push('OpenCode production E2E evidence must include projected log messages');
}
const observedTools = new Set(evidence.mcpTools.observedTools);
const missingTools = evidence.mcpTools.requiredTools.filter((tool) => !observedTools.has(tool));
if (missingTools.length > 0) {
diagnostics.push(
`OpenCode production E2E evidence is missing observed MCP tools: ${missingTools.join(', ')}`
);
}
return diagnostics;
}
function collectExpectedRuntimeDiagnostics(
evidence: OpenCodeProductionE2EEvidence,
expected: OpenCodeProductionE2EGateExpectation
): string[] {
const diagnostics: string[] = [];
if (!expected.opencodeVersion) {
diagnostics.push('OpenCode production gate cannot verify runtime version');
} else if (evidence.version !== expected.opencodeVersion) {
diagnostics.push(
`OpenCode production E2E evidence version ${evidence.version} does not match runtime version ${expected.opencodeVersion}`
);
}
if (!expected.binaryFingerprint) {
diagnostics.push('OpenCode production gate cannot verify runtime binary fingerprint');
} else if (evidence.binaryFingerprint !== expected.binaryFingerprint) {
diagnostics.push(
'OpenCode production E2E evidence binary fingerprint does not match runtime binary fingerprint'
);
}
if (!expected.capabilitySnapshotId) {
diagnostics.push('OpenCode production gate cannot verify capability snapshot id');
} else if (evidence.capabilitySnapshotId !== expected.capabilitySnapshotId) {
diagnostics.push(
'OpenCode production E2E evidence capability snapshot does not match current runtime'
);
}
if (!expected.selectedModel) {
diagnostics.push('OpenCode production gate cannot verify selected raw model id');
} else if (evidence.selectedModel !== expected.selectedModel) {
diagnostics.push(
`OpenCode production E2E evidence model ${evidence.selectedModel} does not match selected model ${expected.selectedModel}`
);
}
const requiredTools = expected.requiredMcpTools ?? [];
if (requiredTools.length > 0) {
const observedTools = new Set(evidence.mcpTools.observedTools);
const missingTools = requiredTools.filter((tool) => !observedTools.has(tool));
if (missingTools.length > 0) {
diagnostics.push(
`OpenCode production E2E evidence does not prove required app MCP tools: ${missingTools.join(', ')}`
);
}
}
return diagnostics;
}
function normalizeRequiredSignals(
value: unknown
): Record<OpenCodeProductionE2ERequiredSignal, boolean> {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence requiredSignals must be an object');
}
return Object.fromEntries(
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [
signal,
requireBoolean(record[signal], `requiredSignals.${signal}`),
])
) as Record<OpenCodeProductionE2ERequiredSignal, boolean>;
}
function normalizeMcpTools(value: unknown): OpenCodeProductionE2EEvidence['mcpTools'] {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence mcpTools must be an object');
}
return {
requiredTools: requireStringArray(record.requiredTools, 'mcpTools.requiredTools'),
observedTools: requireStringArray(record.observedTools, 'mcpTools.observedTools'),
};
}
function normalizeLaunch(value: unknown): OpenCodeProductionE2EEvidence['launch'] {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence launch must be an object');
}
if (record.teamLaunchState !== 'ready') {
throw new Error('OpenCode production E2E evidence launch.teamLaunchState must be ready');
}
return {
runId: requireString(record.runId, 'launch.runId'),
teamId: requireString(record.teamId, 'launch.teamId'),
teamLaunchState: 'ready',
memberCount: requirePositiveInteger(record.memberCount, 'launch.memberCount'),
sessions: requireArray(record.sessions, 'launch.sessions').map(normalizeSession),
durableCheckpoints: requireArray(record.durableCheckpoints, 'launch.durableCheckpoints').map(
normalizeCheckpoint
),
};
}
function normalizeSession(value: unknown): OpenCodeProductionE2ESessionEvidence {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence launch session must be an object');
}
if (record.launchState !== 'confirmed_alive') {
throw new Error('OpenCode production E2E evidence launch session must be confirmed_alive');
}
return {
memberName: requireString(record.memberName, 'launch.sessions.memberName'),
sessionId: requireString(record.sessionId, 'launch.sessions.sessionId'),
launchState: 'confirmed_alive',
};
}
function normalizeCheckpoint(value: unknown): OpenCodeProductionE2ECheckpointEvidence {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence durable checkpoint must be an object');
}
return {
name: requireString(record.name, 'launch.durableCheckpoints.name'),
observedAt: requireIsoDate(record.observedAt, 'launch.durableCheckpoints.observedAt'),
};
}
function normalizeReconcile(value: unknown): OpenCodeProductionE2EEvidence['reconcile'] {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence reconcile must be an object');
}
if (record.teamLaunchState !== 'ready') {
throw new Error('OpenCode production E2E evidence reconcile.teamLaunchState must be ready');
}
return {
runId: requireString(record.runId, 'reconcile.runId'),
teamLaunchState: 'ready',
memberCount: requirePositiveInteger(record.memberCount, 'reconcile.memberCount'),
};
}
function normalizeStop(value: unknown): OpenCodeProductionE2EEvidence['stop'] {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence stop must be an object');
}
if (record.stopped !== true) {
throw new Error('OpenCode production E2E evidence stop.stopped must be true');
}
return {
runId: requireString(record.runId, 'stop.runId'),
stopped: true,
stoppedSessionIds: requireStringArray(record.stoppedSessionIds, 'stop.stoppedSessionIds'),
};
}
function normalizeLogProjection(value: unknown): OpenCodeProductionE2EEvidence['logProjection'] {
const record = asRecord(value);
if (!record) {
throw new Error('OpenCode production E2E evidence logProjection must be an object');
}
if (record.observed !== true) {
throw new Error('OpenCode production E2E evidence logProjection.observed must be true');
}
return {
observed: true,
projectedMessageCount: requirePositiveInteger(
record.projectedMessageCount,
'logProjection.projectedMessageCount'
),
};
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function requireString(value: unknown, field: string): string {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`OpenCode production E2E evidence ${field} must be a non-empty string`);
}
return value.trim();
}
function optionalString(value: unknown, field: string): string | null {
if (value === null || value === undefined) {
return null;
}
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`OpenCode production E2E evidence ${field} must be a non-empty string or null`);
}
return value.trim();
}
function requireBoolean(value: unknown, field: string): boolean {
if (typeof value !== 'boolean') {
throw new Error(`OpenCode production E2E evidence ${field} must be boolean`);
}
return value;
}
function requirePositiveInteger(value: unknown, field: string): number {
if (!Number.isInteger(value) || (value as number) <= 0) {
throw new Error(`OpenCode production E2E evidence ${field} must be a positive integer`);
}
return value as number;
}
function requireIsoDate(value: unknown, field: string): string {
const text = requireString(value, field);
if (!Number.isFinite(Date.parse(text))) {
throw new Error(`OpenCode production E2E evidence ${field} must be an ISO timestamp`);
}
return text;
}
function requireArray(value: unknown, field: string): unknown[] {
if (!Array.isArray(value)) {
throw new Error(`OpenCode production E2E evidence ${field} must be an array`);
}
return value;
}
function requireStringArray(value: unknown, field: string): string[] {
return requireArray(value, field).map((item, index) => requireString(item, `${field}[${index}]`));
}
function optionalStringArray(value: unknown, field: string): string[] | undefined {
if (value === undefined) {
return undefined;
}
return requireStringArray(value, field);
}

View file

@ -0,0 +1,73 @@
import * as path from 'path';
import {
OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION,
validateNullableOpenCodeProductionE2EEvidence,
validateOpenCodeProductionE2EEvidence,
type OpenCodeProductionE2EEvidence,
} from './OpenCodeProductionE2EEvidence';
import { VersionedJsonStore } from '../store/VersionedJsonStore';
export interface OpenCodeProductionE2EEvidenceStoreReadResult {
ok: boolean;
evidence: OpenCodeProductionE2EEvidence | null;
artifactPath: string;
diagnostics: string[];
}
export interface OpenCodeProductionE2EEvidenceStoreOptions {
filePath: string;
clock?: () => Date;
}
export class OpenCodeProductionE2EEvidenceStore {
private readonly filePath: string;
private readonly store: VersionedJsonStore<OpenCodeProductionE2EEvidence | null>;
constructor(options: OpenCodeProductionE2EEvidenceStoreOptions) {
this.filePath = options.filePath;
this.store = new VersionedJsonStore<OpenCodeProductionE2EEvidence | null>({
filePath: options.filePath,
schemaVersion: OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION,
defaultData: () => null,
validate: validateNullableOpenCodeProductionE2EEvidence,
clock: options.clock,
quarantineDir: path.dirname(options.filePath),
});
}
async read(): Promise<OpenCodeProductionE2EEvidenceStoreReadResult> {
const result = await this.store.read();
if (!result.ok) {
return {
ok: false,
evidence: null,
artifactPath: this.filePath,
diagnostics: [
`OpenCode production E2E evidence store is unreadable: ${result.message}`,
...(result.quarantinePath
? [`Quarantined corrupt evidence at ${result.quarantinePath}`]
: []),
],
};
}
return {
ok: true,
evidence: result.data,
artifactPath: this.filePath,
diagnostics:
result.status === 'missing'
? ['OpenCode production E2E evidence artifact has not been written yet']
: [],
};
}
async write(evidence: OpenCodeProductionE2EEvidence): Promise<void> {
const validated = validateOpenCodeProductionE2EEvidence(evidence);
await this.store.updateLocked(() => ({
...validated,
artifactPath: validated.artifactPath ?? this.filePath,
}));
}
}

View file

@ -0,0 +1,413 @@
export type OpenCodeEventScope = 'instance' | 'global';
export type OpenCodeNormalizedStatusType = 'idle' | 'busy' | 'retry' | 'error' | 'unknown';
export interface OpenCodeNormalizedSessionStatus {
type: OpenCodeNormalizedStatusType;
retryAttempt: number | null;
retryMessage: string | null;
retryNextAt: number | null;
rawShape: 'v1.14' | 'legacy-string' | 'unknown';
raw: unknown;
}
export type OpenCodeDurableSessionState =
| 'idle'
| 'running'
| 'retrying'
| 'blocked'
| 'reply_pending'
| 'error'
| 'unknown';
export type OpenCodeNormalizedEvent =
| {
kind: 'server_connected' | 'server_heartbeat';
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'session_status';
sessionId: string;
status: OpenCodeNormalizedSessionStatus;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'session_error';
sessionId: string | null;
errorName: string | null;
errorMessage: string | null;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'message_updated';
sessionId: string;
messageId: string | null;
role: 'assistant' | 'user' | 'system' | 'unknown';
info: Record<string, unknown>;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'message_part_updated';
sessionId: string;
messageId: string | null;
partId: string | null;
partType: string | null;
textSnapshot: string | null;
part: Record<string, unknown>;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'message_part_delta';
sessionId: string;
messageId: string;
partId: string;
field: string;
delta: string;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'message_part_removed';
sessionId: string;
messageId: string;
partId: string;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'permission_asked' | 'permission_replied';
sessionId: string | null;
requestId: string | null;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
| {
kind: 'unknown';
type: string;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
};
export interface OpenCodeSseEventEnvelope {
type: string;
properties: Record<string, unknown>;
scope: OpenCodeEventScope;
directory: string | null;
raw: unknown;
}
export interface OpenCodeDurableStateProjection {
hasPendingPermission: boolean;
hasLatestAssistantError: boolean;
replyPendingSinceMessageId: string | null;
}
export function normalizeOpenCodeSessionStatus(raw: unknown): OpenCodeNormalizedSessionStatus {
if (typeof raw === 'string') {
return {
type: normalizeLegacyStatusType(raw),
retryAttempt: null,
retryMessage: null,
retryNextAt: null,
rawShape: 'legacy-string',
raw,
};
}
const record = asRecord(raw);
const statusType = asString(record?.type);
if (
statusType === 'idle' ||
statusType === 'busy' ||
statusType === 'retry' ||
statusType === 'error'
) {
return {
type: statusType,
retryAttempt: asNumber(record?.attempt),
retryMessage: asString(record?.message),
retryNextAt: asNumber(record?.next),
rawShape: 'v1.14',
raw,
};
}
return {
type: 'unknown',
retryAttempt: null,
retryMessage: null,
retryNextAt: null,
rawShape: 'unknown',
raw,
};
}
export function mapOpenCodeStatusToDurableState(
status: OpenCodeNormalizedSessionStatus | null,
projection: OpenCodeDurableStateProjection
): OpenCodeDurableSessionState {
if (projection.hasPendingPermission) {
return 'blocked';
}
if (projection.hasLatestAssistantError || status?.type === 'error') {
return 'error';
}
if (status?.type === 'retry') {
return 'retrying';
}
if (status?.type === 'busy') {
return 'running';
}
if (projection.replyPendingSinceMessageId) {
return 'reply_pending';
}
if (status?.type === 'idle') {
return 'idle';
}
return 'unknown';
}
export function unwrapOpenCodeEventEnvelope(raw: unknown): OpenCodeSseEventEnvelope | null {
const record = asRecord(raw);
if (!record) {
return null;
}
const directType = asString(record.type);
if (directType) {
return {
type: directType,
properties: asRecord(record.properties) ?? {},
scope: 'instance',
directory: null,
raw,
};
}
const payload = asRecord(record.payload);
const payloadType = asString(payload?.type);
if (!payloadType) {
return null;
}
return {
type: payloadType,
properties: asRecord(payload?.properties) ?? {},
scope: 'global',
directory: asString(record.directory),
raw,
};
}
export function normalizeOpenCodeEvent(raw: unknown): OpenCodeNormalizedEvent | null {
const event = unwrapOpenCodeEventEnvelope(raw);
if (!event) {
return null;
}
const props = event.properties;
if (event.type === 'server.connected' || event.type === 'server.heartbeat') {
return {
kind: event.type === 'server.connected' ? 'server_connected' : 'server_heartbeat',
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'session.status') {
const sessionId = asString(props.sessionID) ?? asString(props.sessionId);
if (!sessionId) {
return unknownEvent(event);
}
return {
kind: 'session_status',
sessionId,
status: normalizeOpenCodeSessionStatus(props.status),
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'session.idle') {
const sessionId = asString(props.sessionID) ?? asString(props.sessionId);
if (!sessionId) {
return unknownEvent(event);
}
return {
kind: 'session_status',
sessionId,
status: normalizeOpenCodeSessionStatus({ type: 'idle' }),
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'session.error') {
const error = asRecord(props.error);
return {
kind: 'session_error',
sessionId: asString(props.sessionID) ?? asString(props.sessionId),
errorName: asString(error?.name) ?? asString(props.name),
errorMessage: asString(error?.message) ?? asString(props.message),
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'message.updated') {
const info = asRecord(props.info) ?? {};
const sessionId =
asString(props.sessionID) ?? asString(props.sessionId) ?? asString(info.sessionID);
if (!sessionId) {
return unknownEvent(event);
}
return {
kind: 'message_updated',
sessionId,
messageId: asString(info.id) ?? asString(info.messageID),
role: normalizeMessageRole(asString(info.role)),
info,
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'message.part.updated') {
const part = asRecord(props.part) ?? {};
const sessionId =
asString(props.sessionID) ?? asString(props.sessionId) ?? asString(part.sessionID);
if (!sessionId) {
return unknownEvent(event);
}
return {
kind: 'message_part_updated',
sessionId,
messageId: asString(part.messageID) ?? asString(part.messageId),
partId: asString(part.id) ?? asString(part.partID) ?? asString(part.partId),
partType: asString(part.type),
textSnapshot: asStringAllowEmpty(part.text),
part,
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'message.part.delta') {
const sessionId = asString(props.sessionID) ?? asString(props.sessionId);
const messageId = asString(props.messageID) ?? asString(props.messageId);
const partId = asString(props.partID) ?? asString(props.partId);
const field = asString(props.field);
const delta = asStringAllowEmpty(props.delta);
if (!sessionId || !messageId || !partId || !field || delta === null) {
return unknownEvent(event);
}
return {
kind: 'message_part_delta',
sessionId,
messageId,
partId,
field,
delta,
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'message.part.removed') {
const sessionId = asString(props.sessionID) ?? asString(props.sessionId);
const messageId = asString(props.messageID) ?? asString(props.messageId);
const partId = asString(props.partID) ?? asString(props.partId);
if (!sessionId || !messageId || !partId) {
return unknownEvent(event);
}
return {
kind: 'message_part_removed',
sessionId,
messageId,
partId,
scope: event.scope,
directory: event.directory,
raw,
};
}
if (event.type === 'permission.asked' || event.type === 'permission.replied') {
return {
kind: event.type === 'permission.asked' ? 'permission_asked' : 'permission_replied',
sessionId: asString(props.sessionID) ?? asString(props.sessionId),
requestId: asString(props.id) ?? asString(props.requestID) ?? asString(props.requestId),
scope: event.scope,
directory: event.directory,
raw,
};
}
return unknownEvent(event);
}
function normalizeLegacyStatusType(raw: string): OpenCodeNormalizedStatusType {
if (raw === 'active') {
return 'busy';
}
if (raw === 'idle' || raw === 'busy' || raw === 'retry' || raw === 'error') {
return raw;
}
return 'unknown';
}
function normalizeMessageRole(role: string | null): 'assistant' | 'user' | 'system' | 'unknown' {
if (role === 'assistant' || role === 'user' || role === 'system') {
return role;
}
return 'unknown';
}
function unknownEvent(event: OpenCodeSseEventEnvelope): OpenCodeNormalizedEvent {
return {
kind: 'unknown',
type: event.type,
scope: event.scope,
directory: event.directory,
raw: event.raw,
};
}
function asString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value : null;
}
function asStringAllowEmpty(value: unknown): string | null {
return typeof value === 'string' ? value : null;
}
function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}

View file

@ -0,0 +1,421 @@
export const REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS = [
'runtime_bootstrap_checkin',
'runtime_deliver_message',
'runtime_task_event',
'runtime_heartbeat',
] as const;
export type RequiredAgentTeamsRuntimeTool = (typeof REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS)[number];
export interface OpenCodeToolListItem {
id: string;
description?: string;
parameters?: unknown;
}
export interface OpenCodeInfrastructureToolClient {
listExperimentalToolIds(): Promise<string[]>;
listExperimentalTools(input: {
providerId: string;
modelId: string;
}): Promise<OpenCodeToolListItem[]>;
}
export type OpenCodeMcpToolProofRoute = '/experimental/tool/ids' | '/experimental/tool' | null;
export interface OpenCodeMcpToolProof {
ok: boolean;
route: OpenCodeMcpToolProofRoute;
canonicalServerName: string;
canonicalExpectedIds: Record<string, string>;
observedTools: string[];
missingTools: string[];
matchedByRequiredTool: Record<string, string | null>;
aliasMatchedByRequiredTool: Record<string, string | null>;
diagnostics: string[];
}
export interface AppMcpRuntimeToolContract {
name: RequiredAgentTeamsRuntimeTool;
requiredInputFields: string[];
idempotencyField: string | null;
runScoped: boolean;
handlerKind: 'bootstrap' | 'delivery' | 'task_event' | 'heartbeat';
}
export interface AppMcpToolDefinition {
name: string;
inputSchema: unknown;
}
export interface AppMcpRuntimeToolPreflightResult {
ok: boolean;
observedToolNames: string[];
diagnostics: string[];
}
export interface RuntimeDeliverMessageSchemaDiagnostic {
severity: 'error';
message: string;
missingFields?: string[];
}
export const APP_MCP_RUNTIME_TOOL_CONTRACTS: AppMcpRuntimeToolContract[] = [
{
name: 'runtime_bootstrap_checkin',
requiredInputFields: ['runId', 'teamName', 'memberName', 'runtimeSessionId'],
idempotencyField: null,
runScoped: true,
handlerKind: 'bootstrap',
},
{
name: 'runtime_deliver_message',
requiredInputFields: [
'idempotencyKey',
'runId',
'teamName',
'fromMemberName',
'runtimeSessionId',
'to',
'text',
],
idempotencyField: 'idempotencyKey',
runScoped: true,
handlerKind: 'delivery',
},
{
name: 'runtime_task_event',
requiredInputFields: ['idempotencyKey', 'runId', 'teamName', 'memberName', 'taskId', 'event'],
idempotencyField: 'idempotencyKey',
runScoped: true,
handlerKind: 'task_event',
},
{
name: 'runtime_heartbeat',
requiredInputFields: ['runId', 'teamName', 'memberName', 'runtimeSessionId'],
idempotencyField: null,
runScoped: true,
handlerKind: 'heartbeat',
},
];
export class OpenCodeMcpToolAvailabilityProbe {
constructor(private readonly client: OpenCodeInfrastructureToolClient) {}
async proveRequiredTools(input: {
serverName: string;
requiredTools: string[];
providerId: string;
modelId: string;
}): Promise<OpenCodeMcpToolProof> {
const idsProof = await this.tryToolIdsProof(input);
if (idsProof.ok) {
return idsProof;
}
const definitionsProof = await this.tryToolDefinitionsProof(input);
if (definitionsProof.ok) {
return definitionsProof;
}
return mergeFailedToolProofs({
serverName: input.serverName,
requiredTools: input.requiredTools,
idsProof,
definitionsProof,
});
}
private async tryToolIdsProof(input: {
serverName: string;
requiredTools: string[];
}): Promise<OpenCodeMcpToolProof> {
try {
const observedTools = await this.client.listExperimentalToolIds();
return matchRequiredOpenCodeTools({
route: '/experimental/tool/ids',
serverName: input.serverName,
requiredTools: input.requiredTools,
observedTools,
});
} catch (error) {
return failedToolProof({
route: '/experimental/tool/ids',
serverName: input.serverName,
requiredTools: input.requiredTools,
diagnostics: [`OpenCode /experimental/tool/ids unavailable - ${stringifyError(error)}`],
});
}
}
private async tryToolDefinitionsProof(input: {
serverName: string;
requiredTools: string[];
providerId: string;
modelId: string;
}): Promise<OpenCodeMcpToolProof> {
try {
const tools = await this.client.listExperimentalTools({
providerId: input.providerId,
modelId: input.modelId,
});
return matchRequiredOpenCodeTools({
route: '/experimental/tool',
serverName: input.serverName,
requiredTools: input.requiredTools,
observedTools: tools.map((tool) => tool.id),
});
} catch (error) {
return failedToolProof({
route: '/experimental/tool',
serverName: input.serverName,
requiredTools: input.requiredTools,
diagnostics: [`OpenCode /experimental/tool unavailable - ${stringifyError(error)}`],
});
}
}
}
export function sanitizeOpenCodeMcpToolPart(value: string): string {
const sanitized = value
.trim()
.replace(/[^a-zA-Z0-9_-]/g, '_')
.replace(/_+/g, '_');
return sanitized.length > 0 ? sanitized : 'unknown';
}
export function buildOpenCodeCanonicalMcpToolId(serverName: string, toolName: string): string {
return `${sanitizeOpenCodeMcpToolPart(serverName)}_${sanitizeOpenCodeMcpToolPart(toolName)}`;
}
export function buildOpenCodeToolIdCandidates(serverName: string, toolName: string): string[] {
const dashServerName = serverName.trim();
const underscoreServerName = sanitizeOpenCodeMcpToolPart(serverName);
const canonical = buildOpenCodeCanonicalMcpToolId(serverName, toolName);
return unique([
canonical,
toolName,
`${dashServerName}:${toolName}`,
`${underscoreServerName}:${toolName}`,
`${dashServerName}_${toolName}`,
`${underscoreServerName}_${toolName}`,
`mcp__${dashServerName}__${toolName}`,
`mcp__${underscoreServerName}__${toolName}`,
]);
}
export function matchRequiredOpenCodeTools(input: {
route: Exclude<OpenCodeMcpToolProofRoute, null>;
serverName: string;
requiredTools: string[];
observedTools: string[];
}): OpenCodeMcpToolProof {
const observed = new Set(input.observedTools);
const matchedByRequiredTool: Record<string, string | null> = {};
const aliasMatchedByRequiredTool: Record<string, string | null> = {};
const missingTools: string[] = [];
const diagnostics: string[] = [];
const canonicalExpectedIds = buildCanonicalExpectedIds(input.serverName, input.requiredTools);
for (const requiredTool of input.requiredTools) {
const canonical = canonicalExpectedIds[requiredTool];
const alias = buildOpenCodeToolIdCandidates(input.serverName, requiredTool).find(
(candidate) => candidate !== canonical && observed.has(candidate)
);
matchedByRequiredTool[requiredTool] = observed.has(canonical) ? canonical : null;
aliasMatchedByRequiredTool[requiredTool] = alias ?? null;
if (!observed.has(canonical)) {
missingTools.push(requiredTool);
diagnostics.push(
alias
? `OpenCode observed alias ${alias} but missing canonical app MCP tool id ${canonical}`
: `OpenCode missing canonical app MCP tool id ${canonical}`
);
}
}
return {
ok: missingTools.length === 0,
route: input.route,
canonicalServerName: sanitizeOpenCodeMcpToolPart(input.serverName),
canonicalExpectedIds,
observedTools: unique(input.observedTools).sort(),
missingTools,
matchedByRequiredTool,
aliasMatchedByRequiredTool,
diagnostics,
};
}
export function verifyAppMcpRuntimeToolContracts(
tools: AppMcpToolDefinition[]
): AppMcpRuntimeToolPreflightResult {
const byName = new Map(tools.map((tool) => [tool.name, tool]));
const diagnostics: string[] = [];
for (const contract of APP_MCP_RUNTIME_TOOL_CONTRACTS) {
const tool = byName.get(contract.name);
if (!tool) {
diagnostics.push(`App MCP tool missing: ${contract.name}`);
continue;
}
const schema = asRecord(tool.inputSchema);
const properties = asRecord(schema?.properties);
const required = asStringArray(schema?.required);
for (const field of contract.requiredInputFields) {
if (!properties?.[field] || !required.includes(field)) {
diagnostics.push(`App MCP tool ${contract.name} missing required field ${field}`);
}
}
if (contract.idempotencyField && !required.includes(contract.idempotencyField)) {
diagnostics.push(
`App MCP tool ${contract.name} idempotency field ${contract.idempotencyField} is not required`
);
}
}
return {
ok: diagnostics.length === 0,
observedToolNames: tools.map((tool) => tool.name).sort(),
diagnostics,
};
}
export function assertRuntimeDeliverMessageSchema(
tools: OpenCodeToolListItem[],
serverName = 'agent-teams'
): RuntimeDeliverMessageSchemaDiagnostic[] {
const deliverToolIds = new Set(
buildOpenCodeToolIdCandidates(serverName, 'runtime_deliver_message')
);
const deliver = tools.find((tool) => deliverToolIds.has(tool.id));
if (!deliver) {
return [{ severity: 'error', message: 'runtime_deliver_message tool is absent' }];
}
const schema = asRecord(deliver.parameters);
const properties = asRecord(schema?.properties);
const required = asStringArray(schema?.required);
const requiredFields = [
'idempotencyKey',
'runId',
'teamName',
'fromMemberName',
'runtimeSessionId',
'to',
'text',
];
const missingFields = requiredFields.filter(
(field) => !properties?.[field] || !required.includes(field)
);
return missingFields.length === 0
? []
: [
{
severity: 'error',
message: `runtime_deliver_message schema missing required fields: ${missingFields.join(', ')}`,
missingFields,
},
];
}
function mergeFailedToolProofs(input: {
serverName: string;
requiredTools: string[];
idsProof: OpenCodeMcpToolProof;
definitionsProof: OpenCodeMcpToolProof;
}): OpenCodeMcpToolProof {
const canonicalExpectedIds = buildCanonicalExpectedIds(input.serverName, input.requiredTools);
const matchedByRequiredTool: Record<string, string | null> = {};
const aliasMatchedByRequiredTool: Record<string, string | null> = {};
for (const tool of input.requiredTools) {
matchedByRequiredTool[tool] =
input.idsProof.matchedByRequiredTool[tool] ??
input.definitionsProof.matchedByRequiredTool[tool] ??
null;
aliasMatchedByRequiredTool[tool] =
input.idsProof.aliasMatchedByRequiredTool[tool] ??
input.definitionsProof.aliasMatchedByRequiredTool[tool] ??
null;
}
return {
ok: false,
route: input.definitionsProof.route ?? input.idsProof.route,
canonicalServerName: sanitizeOpenCodeMcpToolPart(input.serverName),
canonicalExpectedIds,
observedTools: unique([
...input.idsProof.observedTools,
...input.definitionsProof.observedTools,
]).sort(),
missingTools: unique([
...input.idsProof.missingTools,
...input.definitionsProof.missingTools,
]).sort(),
matchedByRequiredTool,
aliasMatchedByRequiredTool,
diagnostics: [
...input.idsProof.diagnostics,
...input.definitionsProof.diagnostics,
'OpenCode app-owned MCP server is connected but required runtime tools were not proven available',
],
};
}
function failedToolProof(input: {
route: Exclude<OpenCodeMcpToolProofRoute, null>;
serverName: string;
requiredTools: string[];
diagnostics: string[];
}): OpenCodeMcpToolProof {
const canonicalExpectedIds = buildCanonicalExpectedIds(input.serverName, input.requiredTools);
return {
ok: false,
route: input.route,
canonicalServerName: sanitizeOpenCodeMcpToolPart(input.serverName),
canonicalExpectedIds,
observedTools: [],
missingTools: [...input.requiredTools],
matchedByRequiredTool: Object.fromEntries(input.requiredTools.map((tool) => [tool, null])),
aliasMatchedByRequiredTool: Object.fromEntries(input.requiredTools.map((tool) => [tool, null])),
diagnostics: input.diagnostics,
};
}
function buildCanonicalExpectedIds(
serverName: string,
requiredTools: string[]
): Record<string, string> {
return Object.fromEntries(
requiredTools.map((tool) => [tool, buildOpenCodeCanonicalMcpToolId(serverName, tool)])
);
}
function unique<T>(items: T[]): T[] {
return [...new Set(items)];
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === 'string');
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,936 @@
import { VersionedJsonStore, VersionedJsonStoreError } from '../store/VersionedJsonStore';
export const RUNTIME_PERMISSION_REQUEST_SCHEMA_VERSION = 1;
export type OpenCodePermissionDecision = 'once' | 'always' | 'reject';
export type OpenCodeRawPermissionRequest = {
id?: unknown;
requestID?: unknown;
sessionID?: unknown;
permission?: unknown;
patterns?: unknown;
metadata?: unknown;
always?: unknown;
tool?: unknown;
title?: unknown;
kind?: unknown;
};
export interface OpenCodeNormalizedPermissionRequest {
requestId: string;
sessionId: string;
permission: string;
patterns: string[];
alwaysPatterns: string[];
toolName: string;
toolCallId: string | null;
messageId: string | null;
title: string;
description: string | null;
metadata: Record<string, unknown>;
rawShape: 'v1.14' | 'legacy' | 'mixed';
raw: OpenCodeRawPermissionRequest;
}
export type RuntimePermissionState =
| 'pending'
| 'answering'
| 'answered'
| 'expired'
| 'stale_run'
| 'provider_missing'
| 'failed_retryable'
| 'failed_terminal';
export type RuntimePermissionAnswerOrigin = 'user_click' | 'provider_side_effect_projection';
export interface RuntimePermissionRequestRecord {
appRequestId: string;
providerRequestId: string;
runId: string;
teamName: string;
memberName: string;
providerId: 'opencode';
runtimeSessionId: string;
permission: string;
patterns: string[];
alwaysPatterns: string[];
toolName: string;
title: string;
description: string | null;
state: RuntimePermissionState;
rawShape: OpenCodeNormalizedPermissionRequest['rawShape'];
requestedAt: string;
updatedAt: string;
expiresAt: string;
answeredAt: string | null;
decision: OpenCodePermissionDecision | null;
answerOrigin: RuntimePermissionAnswerOrigin | null;
lastError: string | null;
}
export type OpenCodePermissionReplySideEffect =
| {
kind: 'answered_clicked_request';
appRequestId: string;
providerRequestId: string;
decision: OpenCodePermissionDecision;
}
| {
kind: 'reject_cancelled_same_session';
appRequestId: string;
providerRequestId: string;
decision: 'reject';
}
| {
kind: 'always_auto_allowed_same_session';
appRequestId: string;
providerRequestId: string;
decision: 'always';
matchedPatterns: string[];
};
export interface RuntimePermissionAnswerProjectionResult {
affectedAppRequestIds: string[];
sideEffects: OpenCodePermissionReplySideEffect[];
}
export interface RuntimePermissionDiagnosticEvent {
type:
| 'opencode_permission_stale_answer_rejected'
| 'opencode_permission_unmatched_session'
| 'opencode_permission_requests_expired'
| 'opencode_permission_answer_failed';
providerId: 'opencode';
teamName: string;
runId: string;
severity: 'info' | 'warning' | 'error';
message: string;
data?: Record<string, unknown>;
createdAt: string;
}
export interface RuntimePermissionDiagnosticsSink {
append(event: RuntimePermissionDiagnosticEvent): Promise<void>;
}
export interface RuntimePermissionLaunchStateStore {
read(teamName: string): Promise<{ runId: string | null } | null>;
updateMember(
teamName: string,
memberName: string,
updater: (member: RuntimePermissionLaunchMemberState) => RuntimePermissionLaunchMemberState
): Promise<void>;
}
export interface RuntimePermissionLaunchMemberState {
launchState?: string;
bootstrapConfirmed?: boolean;
pendingPermissionRequestIds?: string[];
lastRuntimeEventAt?: string;
}
export interface OpenCodePermissionClientPort {
listPendingPermissions(): Promise<OpenCodeNormalizedPermissionRequest[]>;
answerPermission(input: {
requestId: string;
sessionId: string;
decision: OpenCodePermissionDecision;
message?: string;
}): Promise<void>;
}
export interface OpenCodeSessionPermissionRef {
runId: string;
memberName: string;
runtimeSessionId: string;
}
export interface OpenCodePermissionAnswerResult {
ok: boolean;
requestId: string;
diagnostics: string[];
}
export class RuntimePermissionRequestStore {
constructor(private readonly store: VersionedJsonStore<RuntimePermissionRequestRecord[]>) {}
async upsertPending(
input: RuntimePermissionRequestRecord
): Promise<'created' | 'updated' | 'unchanged'> {
let outcome: 'created' | 'updated' | 'unchanged' = 'created';
await this.store.updateLocked((records) => {
const index = records.findIndex((record) => record.appRequestId === input.appRequestId);
if (index < 0) {
return [...records, input];
}
const current = records[index];
if (current.state === 'answered') {
if (current.answerOrigin !== 'provider_side_effect_projection') {
outcome = 'unchanged';
return records;
}
const reopened = {
...current,
...input,
requestedAt: current.requestedAt,
answeredAt: null,
decision: null,
answerOrigin: null,
lastError: null,
};
outcome =
stablePermissionRecordJson(current) === stablePermissionRecordJson(reopened)
? 'unchanged'
: 'updated';
return records.map((record, recordIndex) => (recordIndex === index ? reopened : record));
}
const next = {
...current,
...input,
requestedAt: current.requestedAt,
answeredAt: current.answeredAt,
decision: current.decision,
answerOrigin: current.answerOrigin,
lastError: null,
};
outcome =
stablePermissionRecordJson(current) === stablePermissionRecordJson(next)
? 'unchanged'
: 'updated';
return records.map((record, recordIndex) => (recordIndex === index ? next : record));
});
return outcome;
}
async beginAnswer(input: {
appRequestId: string;
runId: string;
now: string;
}): Promise<
| { state: 'locked'; record: RuntimePermissionRequestRecord }
| { state: 'missing' }
| { state: 'stale_run'; record: RuntimePermissionRequestRecord }
| { state: 'already_answered'; record: RuntimePermissionRequestRecord }
| { state: 'already_answering'; record: RuntimePermissionRequestRecord }
> {
let result:
| { state: 'locked'; record: RuntimePermissionRequestRecord }
| { state: 'missing' }
| { state: 'stale_run'; record: RuntimePermissionRequestRecord }
| { state: 'already_answered'; record: RuntimePermissionRequestRecord }
| { state: 'already_answering'; record: RuntimePermissionRequestRecord }
| null = null;
await this.store.updateLocked((records) => {
const existing = records.find((record) => record.appRequestId === input.appRequestId);
if (!existing) {
result = { state: 'missing' };
return records;
}
if (existing.runId !== input.runId) {
result = { state: 'stale_run', record: existing };
return records;
}
if (existing.state === 'answered') {
result = { state: 'already_answered', record: existing };
return records;
}
if (existing.state === 'answering') {
result = { state: 'already_answering', record: existing };
return records;
}
const locked = {
...existing,
state: 'answering' as const,
updatedAt: input.now,
lastError: null,
};
result = { state: 'locked', record: locked };
return records.map((record) =>
record.appRequestId === input.appRequestId ? locked : record
);
});
if (!result) {
throw new Error('Runtime permission begin answer failed');
}
return result;
}
async markAnsweredWithSideEffects(input: {
appRequestId: string;
decision: OpenCodePermissionDecision;
answeredAt: string;
}): Promise<RuntimePermissionAnswerProjectionResult> {
let result: RuntimePermissionAnswerProjectionResult | null = null;
await this.store.updateLocked((records) => {
const clicked = records.find((record) => record.appRequestId === input.appRequestId);
if (!clicked) {
throw new Error(`Runtime permission request not found: ${input.appRequestId}`);
}
const affectedAppRequestIds = new Set<string>([input.appRequestId]);
const sideEffects: OpenCodePermissionReplySideEffect[] = [
{
kind: 'answered_clicked_request',
appRequestId: clicked.appRequestId,
providerRequestId: clicked.providerRequestId,
decision: input.decision,
},
];
const nextRecords = records.map((record) => {
if (record.appRequestId === input.appRequestId) {
return answerPermissionRecord({
record,
decision: input.decision,
answeredAt: input.answeredAt,
answerOrigin: 'user_click',
});
}
if (!isProjectableProviderSideEffectPeer(clicked, record)) {
return record;
}
if (input.decision === 'reject') {
affectedAppRequestIds.add(record.appRequestId);
sideEffects.push({
kind: 'reject_cancelled_same_session',
appRequestId: record.appRequestId,
providerRequestId: record.providerRequestId,
decision: 'reject',
});
return answerPermissionRecord({
record,
decision: 'reject',
answeredAt: input.answeredAt,
answerOrigin: 'provider_side_effect_projection',
});
}
if (input.decision === 'always') {
const matchedPatterns = findAlwaysProjectionMatches(clicked, record);
if (matchedPatterns.length === 0) {
return record;
}
affectedAppRequestIds.add(record.appRequestId);
sideEffects.push({
kind: 'always_auto_allowed_same_session',
appRequestId: record.appRequestId,
providerRequestId: record.providerRequestId,
decision: 'always',
matchedPatterns,
});
return answerPermissionRecord({
record,
decision: 'always',
answeredAt: input.answeredAt,
answerOrigin: 'provider_side_effect_projection',
});
}
return record;
});
result = {
affectedAppRequestIds: [...affectedAppRequestIds],
sideEffects,
};
return nextRecords;
});
if (!result) {
throw new Error('Runtime permission answer projection failed');
}
return result;
}
async markFailed(input: {
appRequestId: string;
state: 'failed_retryable' | 'failed_terminal' | 'provider_missing';
error: string;
updatedAt: string;
}): Promise<void> {
await this.updateExisting(input.appRequestId, (record) => ({
...record,
state: input.state,
updatedAt: input.updatedAt,
lastError: input.error,
}));
}
async expireMissingProviderRequests(input: {
runId: string;
teamName: string;
visibleProviderRequestIds: Set<string>;
now: string;
}): Promise<string[]> {
const expired: string[] = [];
await this.store.updateLocked((records) =>
records.map((record) => {
if (
record.runId !== input.runId ||
record.teamName !== input.teamName ||
record.state !== 'pending' ||
input.visibleProviderRequestIds.has(record.providerRequestId)
) {
return record;
}
expired.push(record.appRequestId);
return {
...record,
state: 'provider_missing' as const,
updatedAt: input.now,
lastError: 'Provider no longer lists this permission request',
};
})
);
return expired;
}
async listPendingForTeam(teamName: string): Promise<RuntimePermissionRequestRecord[]> {
const records = await this.readRequired();
return records.filter((record) => record.teamName === teamName && record.state === 'pending');
}
async get(appRequestId: string): Promise<RuntimePermissionRequestRecord | null> {
const records = await this.readRequired();
return records.find((record) => record.appRequestId === appRequestId) ?? null;
}
async list(): Promise<RuntimePermissionRequestRecord[]> {
return this.readRequired();
}
private async updateExisting(
appRequestId: string,
updater: (record: RuntimePermissionRequestRecord) => RuntimePermissionRequestRecord
): Promise<void> {
let found = false;
await this.store.updateLocked((records) =>
records.map((record) => {
if (record.appRequestId !== appRequestId) {
return record;
}
found = true;
return updater(record);
})
);
if (!found) {
throw new Error(`Runtime permission request not found: ${appRequestId}`);
}
}
private async readRequired(): Promise<RuntimePermissionRequestRecord[]> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
return result.data;
}
}
export class RuntimePermissionAnswerService {
constructor(
private readonly store: RuntimePermissionRequestStore,
private readonly launchStateStore: RuntimePermissionLaunchStateStore,
private readonly openCodeClient: OpenCodePermissionClientPort,
private readonly diagnostics: RuntimePermissionDiagnosticsSink,
private readonly clock: () => Date = () => new Date()
) {}
async answer(input: {
appRequestId: string;
runId: string;
decision: OpenCodePermissionDecision;
message?: string;
}): Promise<OpenCodePermissionAnswerResult> {
const now = this.clock().toISOString();
const begin = await this.store.beginAnswer({
appRequestId: input.appRequestId,
runId: input.runId,
now,
});
if (begin.state === 'missing') {
return {
ok: false,
requestId: input.appRequestId,
diagnostics: ['Permission request not found'],
};
}
if (begin.state === 'stale_run') {
await this.diagnostics.append({
type: 'opencode_permission_stale_answer_rejected',
providerId: 'opencode',
teamName: begin.record.teamName,
runId: input.runId,
severity: 'warning',
message: 'OpenCode permission answer rejected because request belongs to another run',
data: { appRequestId: input.appRequestId, requestRunId: begin.record.runId },
createdAt: now,
});
return { ok: false, requestId: input.appRequestId, diagnostics: ['Stale runId rejected'] };
}
if (begin.state === 'already_answered') {
return {
ok: true,
requestId: input.appRequestId,
diagnostics: ['Permission already answered'],
};
}
if (begin.state === 'already_answering') {
return {
ok: false,
requestId: input.appRequestId,
diagnostics: ['Permission answer already in progress'],
};
}
const record = begin.record;
const launchState = await this.launchStateStore.read(record.teamName);
if (launchState?.runId !== record.runId) {
await this.store.markFailed({
appRequestId: record.appRequestId,
state: 'failed_terminal',
error: 'Launch state moved to another run before permission answer',
updatedAt: now,
});
return {
ok: false,
requestId: record.appRequestId,
diagnostics: ['Launch state moved to another run'],
};
}
try {
await this.openCodeClient.answerPermission({
requestId: record.providerRequestId,
sessionId: record.runtimeSessionId,
decision: input.decision,
message: input.message,
});
const answeredAt = this.clock().toISOString();
await this.store.markAnsweredWithSideEffects({
appRequestId: record.appRequestId,
decision: input.decision,
answeredAt,
});
const remainingMemberPendingIds = (await this.store.listPendingForTeam(record.teamName))
.filter(
(pendingRecord) =>
pendingRecord.runId === record.runId && pendingRecord.memberName === record.memberName
)
.map((pendingRecord) => pendingRecord.appRequestId);
await this.launchStateStore.updateMember(record.teamName, record.memberName, (member) => ({
...member,
pendingPermissionRequestIds: remainingMemberPendingIds,
lastRuntimeEventAt: answeredAt,
}));
return { ok: true, requestId: record.appRequestId, diagnostics: [] };
} catch (error) {
await this.store.markFailed({
appRequestId: record.appRequestId,
state: 'failed_retryable',
error: stringifyError(error),
updatedAt: this.clock().toISOString(),
});
await this.diagnostics.append({
type: 'opencode_permission_answer_failed',
providerId: 'opencode',
teamName: record.teamName,
runId: record.runId,
severity: 'warning',
message: 'OpenCode permission answer failed and remains retryable',
data: { appRequestId: record.appRequestId, error: stringifyError(error) },
createdAt: this.clock().toISOString(),
});
throw error;
}
}
}
export class RuntimePermissionReconciler {
constructor(
private readonly client: OpenCodePermissionClientPort,
private readonly store: RuntimePermissionRequestStore,
private readonly launchStateStore: RuntimePermissionLaunchStateStore,
private readonly diagnostics: RuntimePermissionDiagnosticsSink,
private readonly clock: () => Date = () => new Date()
) {}
async reconcile(input: {
runId: string;
teamName: string;
sessionsByOpenCodeId: Map<string, OpenCodeSessionPermissionRef>;
}): Promise<void> {
const now = this.clock().toISOString();
const pending = await this.client.listPendingPermissions();
const visibleProviderRequestIds = new Set<string>();
const pendingByMember = new Map<string, string[]>();
for (const permission of pending) {
visibleProviderRequestIds.add(permission.requestId);
const session = input.sessionsByOpenCodeId.get(permission.sessionId);
if (!session || session.runId !== input.runId) {
await this.diagnostics.append({
type: 'opencode_permission_unmatched_session',
providerId: 'opencode',
teamName: input.teamName,
runId: input.runId,
severity: 'warning',
message: 'OpenCode permission request did not match a current runtime session',
data: { providerRequestId: permission.requestId, sessionId: permission.sessionId },
createdAt: now,
});
continue;
}
const appRequestId = createOpenCodePermissionAppRequestId(input.runId, permission.requestId);
await this.store.upsertPending({
appRequestId,
providerRequestId: permission.requestId,
runId: input.runId,
teamName: input.teamName,
memberName: session.memberName,
providerId: 'opencode',
runtimeSessionId: permission.sessionId,
permission: permission.permission,
patterns: permission.patterns,
alwaysPatterns: permission.alwaysPatterns,
toolName: permission.toolName,
title: permission.title,
description: permission.description,
state: 'pending',
rawShape: permission.rawShape,
requestedAt: now,
updatedAt: now,
expiresAt: new Date(Date.parse(now) + 15 * 60_000).toISOString(),
answeredAt: null,
decision: null,
answerOrigin: null,
lastError: null,
});
pendingByMember.set(session.memberName, [
...(pendingByMember.get(session.memberName) ?? []),
appRequestId,
]);
}
const expired = await this.store.expireMissingProviderRequests({
runId: input.runId,
teamName: input.teamName,
visibleProviderRequestIds,
now,
});
if (expired.length > 0) {
await this.diagnostics.append({
type: 'opencode_permission_requests_expired',
providerId: 'opencode',
teamName: input.teamName,
runId: input.runId,
severity: 'info',
message: 'OpenCode permission requests disappeared from provider and were expired locally',
data: { expiredCount: expired.length },
createdAt: now,
});
}
for (const [memberName, requestIds] of pendingByMember) {
await this.launchStateStore.updateMember(input.teamName, memberName, (member) => ({
...member,
launchState:
member.launchState === 'confirmed_alive'
? member.launchState
: 'runtime_pending_permission',
pendingPermissionRequestIds: [...new Set(requestIds)],
lastRuntimeEventAt: now,
}));
}
}
}
export function createRuntimePermissionRequestStore(options: {
filePath: string;
clock?: () => Date;
}): RuntimePermissionRequestStore {
const clock = options.clock ?? (() => new Date());
return new RuntimePermissionRequestStore(
new VersionedJsonStore<RuntimePermissionRequestRecord[]>({
filePath: options.filePath,
schemaVersion: RUNTIME_PERMISSION_REQUEST_SCHEMA_VERSION,
defaultData: () => [],
validate: validateRuntimePermissionRequestRecords,
clock,
})
);
}
export function normalizeOpenCodePermissionRequest(
raw: OpenCodeRawPermissionRequest
): OpenCodeNormalizedPermissionRequest | null {
const requestId = asString(raw.id) ?? asString(raw.requestID);
const sessionId = asString(raw.sessionID);
if (!requestId || !sessionId) {
return null;
}
const toolObject = isRecord(raw.tool) ? raw.tool : null;
const legacyToolName = asString(raw.tool);
const permission = asString(raw.permission) ?? asString(raw.kind) ?? legacyToolName ?? 'unknown';
const patterns = asStringArray(raw.patterns);
const alwaysPatterns = asStringArray(raw.always);
const metadata = asRecord(raw.metadata);
const toolName =
legacyToolName ?? asString(toolObject?.name) ?? asString(metadata.toolName) ?? permission;
const messageId = asString(toolObject?.messageID) ?? asString(metadata.messageID);
const toolCallId = asString(toolObject?.callID) ?? asString(metadata.callID);
return {
requestId,
sessionId,
permission,
patterns,
alwaysPatterns,
toolName,
toolCallId,
messageId,
title: asString(raw.title) ?? buildOpenCodePermissionTitle({ permission, toolName, patterns }),
description:
asString(raw.kind) ??
buildOpenCodePermissionDescription({ patterns, alwaysPatterns, metadata }),
metadata,
rawShape: detectPermissionRawShape(raw),
raw,
};
}
export function createOpenCodePermissionAppRequestId(
runId: string,
providerRequestId: string
): string {
return `opencode:${runId}:${providerRequestId}`;
}
export function validateRuntimePermissionRequestRecords(
value: unknown
): RuntimePermissionRequestRecord[] {
if (!Array.isArray(value)) {
throw new Error('Runtime permission requests must be an array');
}
const seen = new Set<string>();
return value.map((record, index) => {
if (!isRuntimePermissionRequestRecord(record)) {
throw new Error(`Invalid runtime permission request at index ${index}`);
}
const normalized = normalizeRuntimePermissionRequestRecord(record);
if (seen.has(normalized.appRequestId)) {
throw new Error(`Duplicate runtime permission request id: ${normalized.appRequestId}`);
}
seen.add(normalized.appRequestId);
return normalized;
});
}
function detectPermissionRawShape(
raw: OpenCodeRawPermissionRequest
): OpenCodeNormalizedPermissionRequest['rawShape'] {
const hasV114Fields =
typeof raw.id === 'string' || typeof raw.permission === 'string' || Array.isArray(raw.patterns);
const hasLegacyFields =
typeof raw.requestID === 'string' ||
typeof raw.title === 'string' ||
typeof raw.kind === 'string';
if (hasV114Fields && hasLegacyFields) {
return 'mixed';
}
if (hasV114Fields) {
return 'v1.14';
}
return 'legacy';
}
function buildOpenCodePermissionTitle(input: {
permission: string;
toolName: string;
patterns: string[];
}): string {
if (input.patterns.length > 0) {
return `OpenCode wants ${input.permission} permission for ${input.patterns[0]}`;
}
if (input.toolName !== 'unknown') {
return `OpenCode wants to use ${input.toolName}`;
}
return `OpenCode permission request: ${input.permission}`;
}
function buildOpenCodePermissionDescription(input: {
patterns: string[];
alwaysPatterns: string[];
metadata: Record<string, unknown>;
}): string | null {
const parts: string[] = [];
if (input.patterns.length > 0) {
parts.push(`Patterns: ${input.patterns.join(', ')}`);
}
if (input.alwaysPatterns.length > 0) {
parts.push(`Always candidates: ${input.alwaysPatterns.join(', ')}`);
}
const reason = asString(input.metadata.reason);
if (reason) {
parts.push(`Reason: ${reason}`);
}
return parts.length > 0 ? parts.join('\n') : null;
}
function answerPermissionRecord(input: {
record: RuntimePermissionRequestRecord;
decision: OpenCodePermissionDecision;
answeredAt: string;
answerOrigin: RuntimePermissionAnswerOrigin;
}): RuntimePermissionRequestRecord {
return {
...input.record,
state: 'answered',
answeredAt: input.answeredAt,
decision: input.decision,
answerOrigin: input.answerOrigin,
updatedAt: input.answeredAt,
lastError: null,
};
}
function isProjectableProviderSideEffectPeer(
clicked: RuntimePermissionRequestRecord,
candidate: RuntimePermissionRequestRecord
): boolean {
return (
candidate.appRequestId !== clicked.appRequestId &&
candidate.providerId === 'opencode' &&
candidate.runId === clicked.runId &&
candidate.teamName === clicked.teamName &&
candidate.runtimeSessionId === clicked.runtimeSessionId &&
candidate.state === 'pending'
);
}
function findAlwaysProjectionMatches(
clicked: RuntimePermissionRequestRecord,
candidate: RuntimePermissionRequestRecord
): string[] {
const allowedPatterns = new Set([...clicked.alwaysPatterns, ...clicked.patterns]);
if (allowedPatterns.size === 0) {
return [];
}
return [...new Set(candidate.patterns.filter((pattern) => allowedPatterns.has(pattern)))];
}
function normalizeRuntimePermissionRequestRecord(
record: RuntimePermissionRequestRecord
): RuntimePermissionRequestRecord {
return {
...record,
permission: isNonEmptyString(record.permission) ? record.permission : record.toolName,
patterns: isStringArray(record.patterns) ? record.patterns : [],
alwaysPatterns: isStringArray(record.alwaysPatterns) ? record.alwaysPatterns : [],
answerOrigin: isRuntimePermissionAnswerOrigin(record.answerOrigin) ? record.answerOrigin : null,
};
}
function isRuntimePermissionRequestRecord(value: unknown): value is RuntimePermissionRequestRecord {
return (
isRecord(value) &&
isNonEmptyString(value.appRequestId) &&
isNonEmptyString(value.providerRequestId) &&
isNonEmptyString(value.runId) &&
isNonEmptyString(value.teamName) &&
isNonEmptyString(value.memberName) &&
value.providerId === 'opencode' &&
isNonEmptyString(value.runtimeSessionId) &&
(value.permission === undefined || isNonEmptyString(value.permission)) &&
(value.patterns === undefined || isStringArray(value.patterns)) &&
(value.alwaysPatterns === undefined || isStringArray(value.alwaysPatterns)) &&
isNonEmptyString(value.toolName) &&
isNonEmptyString(value.title) &&
(value.description === null || typeof value.description === 'string') &&
isRuntimePermissionState(value.state) &&
(value.rawShape === 'v1.14' || value.rawShape === 'legacy' || value.rawShape === 'mixed') &&
isNonEmptyString(value.requestedAt) &&
isNonEmptyString(value.updatedAt) &&
isNonEmptyString(value.expiresAt) &&
(value.answeredAt === null || isNonEmptyString(value.answeredAt)) &&
(value.decision === null || isOpenCodePermissionDecision(value.decision)) &&
(value.answerOrigin === undefined ||
value.answerOrigin === null ||
isRuntimePermissionAnswerOrigin(value.answerOrigin)) &&
(value.lastError === null || typeof value.lastError === 'string')
);
}
function isRuntimePermissionState(value: unknown): value is RuntimePermissionState {
return (
value === 'pending' ||
value === 'answering' ||
value === 'answered' ||
value === 'expired' ||
value === 'stale_run' ||
value === 'provider_missing' ||
value === 'failed_retryable' ||
value === 'failed_terminal'
);
}
function isOpenCodePermissionDecision(value: unknown): value is OpenCodePermissionDecision {
return value === 'once' || value === 'always' || value === 'reject';
}
function isRuntimePermissionAnswerOrigin(value: unknown): value is RuntimePermissionAnswerOrigin {
return value === 'user_click' || value === 'provider_side_effect_projection';
}
function stablePermissionRecordJson(value: RuntimePermissionRequestRecord): string {
return JSON.stringify(value);
}
function asString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value : null;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
function asRecord(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,378 @@
import type { OpenCodeApiCapabilities } from '../capabilities/OpenCodeApiCapabilities';
import type { OpenCodeTeamLaunchMode } from '../bridge/OpenCodeBridgeCommandContract';
import type { OpenCodeMcpToolProof } from '../mcp/OpenCodeMcpToolAvailability';
import {
evaluateOpenCodeSupport,
OPENCODE_TEAM_LAUNCH_VERSION_POLICY,
type OpenCodeInstallMethod,
type OpenCodeProductionE2EEvidence,
type OpenCodeSupportLevel,
type OpenCodeSupportedVersionPolicy,
} from '../version/OpenCodeVersionPolicy';
import type { RuntimeStoreReadinessCheck } from '../store/RuntimeStoreManifest';
export type OpenCodeTeamLaunchReadinessState =
| 'ready'
| 'not_installed'
| 'not_authenticated'
| 'unsupported_version'
| 'capabilities_missing'
| 'e2e_missing'
| 'runtime_store_blocked'
| 'mcp_unavailable'
| 'model_unavailable'
| 'adapter_disabled'
| 'unknown_error';
export interface OpenCodeRuntimeInventory {
detected: boolean;
binaryPath: string | null;
installMethod: OpenCodeInstallMethod;
version: string | null;
authenticated: boolean;
connectedProviders: string[];
models: string[];
diagnostics: string[];
}
export interface OpenCodeModelExecutionProbeResult {
outcome: 'available' | 'unavailable' | 'unknown';
reason: string | null;
diagnostics: string[];
}
export interface OpenCodeTeamLaunchReadiness {
state: OpenCodeTeamLaunchReadinessState;
launchAllowed: boolean;
modelId: string | null;
opencodeVersion: string | null;
installMethod: OpenCodeInstallMethod | null;
binaryPath: string | null;
hostHealthy: boolean;
appMcpConnected: boolean;
requiredToolsPresent: boolean;
permissionBridgeReady: boolean;
runtimeStoresReady: boolean;
supportLevel: OpenCodeSupportLevel | null;
missing: string[];
diagnostics: string[];
evidence: {
capabilitiesReady: boolean;
mcpToolProofRoute: OpenCodeMcpToolProof['route'];
observedMcpTools: string[];
runtimeStoreReadinessReason: RuntimeStoreReadinessCheck['reason'] | null;
};
}
export interface OpenCodeRuntimeInventoryPort {
probe(input: { projectPath: string }): Promise<OpenCodeRuntimeInventory>;
}
export interface OpenCodeApiCapabilityPort {
detect(input: {
projectPath: string;
inventory: OpenCodeRuntimeInventory;
}): Promise<OpenCodeApiCapabilities>;
}
export interface OpenCodeMcpToolProofPort {
prove(input: {
projectPath: string;
modelId: string;
inventory: OpenCodeRuntimeInventory;
capabilities: OpenCodeApiCapabilities;
}): Promise<OpenCodeMcpToolProof>;
}
export interface OpenCodeRuntimeStoreReadinessPort {
check(input: { projectPath: string }): Promise<RuntimeStoreReadinessCheck>;
}
export interface OpenCodeModelExecutionProbePort {
verify(input: {
projectPath: string;
modelId: string;
inventory: OpenCodeRuntimeInventory;
}): Promise<OpenCodeModelExecutionProbeResult>;
}
export interface OpenCodeProductionE2EEvidencePort {
read(input: {
projectPath: string;
inventory: OpenCodeRuntimeInventory;
capabilities: OpenCodeApiCapabilities;
}): Promise<OpenCodeProductionE2EEvidence | null>;
}
export interface OpenCodeTeamLaunchReadinessServiceOptions {
versionPolicy?: OpenCodeSupportedVersionPolicy;
launchMode?: OpenCodeTeamLaunchMode;
/**
* @deprecated Use launchMode. Kept for callers that still pass a boolean feature gate.
*/
adapterEnabled?: boolean;
}
export class OpenCodeTeamLaunchReadinessService {
constructor(
private readonly inventory: OpenCodeRuntimeInventoryPort,
private readonly capabilities: OpenCodeApiCapabilityPort,
private readonly mcpTools: OpenCodeMcpToolProofPort,
private readonly runtimeStores: OpenCodeRuntimeStoreReadinessPort,
private readonly modelExecution: OpenCodeModelExecutionProbePort,
private readonly e2eEvidence: OpenCodeProductionE2EEvidencePort,
private readonly options: OpenCodeTeamLaunchReadinessServiceOptions = {}
) {}
async check(input: {
projectPath: string;
selectedModel: string | null;
requireExecutionProbe: boolean;
launchMode?: OpenCodeTeamLaunchMode;
}): Promise<OpenCodeTeamLaunchReadiness> {
const launchMode = resolveReadinessLaunchMode(input.launchMode, this.options);
const policy = this.options.versionPolicy ?? OPENCODE_TEAM_LAUNCH_VERSION_POLICY;
const dogfoodWarnings: string[] = [];
if (launchMode === 'disabled') {
return readiness({
state: 'adapter_disabled',
inventory: null,
modelId: input.selectedModel,
missing: ['OpenCode team launch adapter is disabled by feature gate'],
diagnostics: ['OpenCode team launch adapter is disabled by feature gate'],
});
}
try {
const inventory = await this.inventory.probe({ projectPath: input.projectPath });
if (!inventory.detected) {
return readiness({
state: 'not_installed',
inventory,
modelId: input.selectedModel,
diagnostics: appendDiagnostics(inventory.diagnostics, [
'OpenCode CLI not detected on PATH',
]),
});
}
if (!inventory.authenticated || inventory.connectedProviders.length === 0) {
return readiness({
state: 'not_authenticated',
inventory,
modelId: input.selectedModel,
diagnostics: appendDiagnostics(inventory.diagnostics, [
'No connected OpenCode providers found',
]),
});
}
const modelId = input.selectedModel ?? inventory.models[0] ?? null;
if (!modelId) {
return readiness({
state: 'model_unavailable',
inventory,
modelId: null,
diagnostics: appendDiagnostics(inventory.diagnostics, ['No OpenCode model is available']),
});
}
const capabilities = await this.capabilities.detect({
projectPath: input.projectPath,
inventory,
});
const evidence = await this.e2eEvidence.read({
projectPath: input.projectPath,
inventory,
capabilities,
});
const support = evaluateOpenCodeSupport({
version: inventory.version ?? '0.0.0',
capabilities,
evidence,
policy,
});
if (!support.supported) {
if (launchMode === 'dogfood' && support.supportLevel === 'supported_e2e_pending') {
dogfoodWarnings.push(
'OpenCode production E2E evidence is missing; dogfood launch remains allowed after runtime checks.'
);
} else {
return readiness({
state: mapSupportLevelToReadinessState(support.supportLevel),
inventory,
modelId,
capabilities,
supportLevel: support.supportLevel,
missing: support.diagnostics,
diagnostics: appendDiagnostics(inventory.diagnostics, support.diagnostics),
});
}
}
const runtimeStoreReadiness = await this.runtimeStores.check({
projectPath: input.projectPath,
});
if (!runtimeStoreReadiness.ok) {
return readiness({
state: 'runtime_store_blocked',
inventory,
modelId,
capabilities,
runtimeStoreReadiness,
supportLevel: support.supportLevel,
missing: runtimeStoreReadiness.diagnostics,
diagnostics: appendDiagnostics(inventory.diagnostics, runtimeStoreReadiness.diagnostics),
});
}
const toolProof = await this.mcpTools.prove({
projectPath: input.projectPath,
modelId,
inventory,
capabilities,
});
if (!toolProof.ok) {
return readiness({
state: 'mcp_unavailable',
inventory,
modelId,
capabilities,
toolProof,
runtimeStoreReadiness,
supportLevel: support.supportLevel,
missing: toolProof.missingTools,
diagnostics: appendDiagnostics(inventory.diagnostics, toolProof.diagnostics),
});
}
if (input.requireExecutionProbe) {
const modelProbe = await this.modelExecution.verify({
projectPath: input.projectPath,
modelId,
inventory,
});
if (modelProbe.outcome !== 'available') {
return readiness({
state: 'model_unavailable',
inventory,
modelId,
capabilities,
toolProof,
runtimeStoreReadiness,
supportLevel: support.supportLevel,
missing: [modelProbe.reason ?? 'OpenCode selected model execution is unavailable'],
diagnostics: appendDiagnostics(inventory.diagnostics, modelProbe.diagnostics),
});
}
}
return readiness({
state: 'ready',
inventory,
modelId,
capabilities,
toolProof,
runtimeStoreReadiness,
supportLevel: support.supportLevel,
launchAllowed: true,
diagnostics: appendDiagnostics(inventory.diagnostics, dogfoodWarnings),
});
} catch (error) {
return readiness({
state: 'unknown_error',
inventory: null,
modelId: input.selectedModel,
diagnostics: [`OpenCode readiness check failed: ${stringifyError(error)}`],
});
}
}
}
function resolveReadinessLaunchMode(
requested: OpenCodeTeamLaunchMode | undefined,
options: OpenCodeTeamLaunchReadinessServiceOptions
): OpenCodeTeamLaunchMode {
if (requested) {
return requested;
}
if (options.launchMode) {
return options.launchMode;
}
if (options.adapterEnabled === true) {
return 'production';
}
return 'disabled';
}
function readiness(input: {
state: OpenCodeTeamLaunchReadinessState;
inventory: OpenCodeRuntimeInventory | null;
modelId: string | null;
capabilities?: OpenCodeApiCapabilities;
toolProof?: OpenCodeMcpToolProof;
runtimeStoreReadiness?: RuntimeStoreReadinessCheck;
supportLevel?: OpenCodeSupportLevel | null;
launchAllowed?: boolean;
missing?: string[];
diagnostics: string[];
}): OpenCodeTeamLaunchReadiness {
const toolProof = input.toolProof ?? null;
const capabilitiesReady = input.capabilities?.requiredForTeamLaunch.ready === true;
return {
state: input.state,
launchAllowed: input.launchAllowed === true,
modelId: input.modelId,
opencodeVersion: input.inventory?.version ?? null,
installMethod: input.inventory?.installMethod ?? null,
binaryPath: input.inventory?.binaryPath ?? null,
hostHealthy: input.inventory?.detected === true,
appMcpConnected: toolProof !== null,
requiredToolsPresent: toolProof?.ok === true,
permissionBridgeReady:
input.capabilities?.endpoints.permissionList === true &&
(input.capabilities.endpoints.permissionReply === true ||
input.capabilities.endpoints.permissionLegacySessionRespond === true),
runtimeStoresReady: input.runtimeStoreReadiness?.ok === true,
supportLevel: input.supportLevel ?? null,
missing: dedupe(input.missing ?? []),
diagnostics: dedupe(input.diagnostics),
evidence: {
capabilitiesReady,
mcpToolProofRoute: toolProof?.route ?? null,
observedMcpTools: toolProof?.observedTools ?? [],
runtimeStoreReadinessReason: input.runtimeStoreReadiness?.reason ?? null,
},
};
}
function mapSupportLevelToReadinessState(
supportLevel: OpenCodeSupportLevel
): OpenCodeTeamLaunchReadinessState {
switch (supportLevel) {
case 'unsupported_too_old':
case 'unsupported_prerelease':
return 'unsupported_version';
case 'supported_capabilities_pending':
return 'capabilities_missing';
case 'supported_e2e_pending':
return 'e2e_missing';
case 'production_supported':
return 'ready';
}
}
function appendDiagnostics(left: string[], right: string[]): string[] {
return dedupe([...left, ...right]);
}
function dedupe(values: string[]): string[] {
return [...new Set(values.filter((value) => value.trim().length > 0))];
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,399 @@
import { createHash } from 'crypto';
import { stableJsonStringify } from '../bridge/OpenCodeBridgeCommandContract';
import { VersionedJsonStore, VersionedJsonStoreError } from './VersionedJsonStore';
export const OPENCODE_LAUNCH_TRANSACTION_SCHEMA_VERSION = 1;
export type OpenCodeLaunchCheckpointName =
| 'run_created'
| 'host_ready'
| 'lead_session_recorded'
| 'member_session_recorded'
| 'mcp_connected'
| 'required_tools_proven'
| 'prompt_sent'
| 'bootstrap_confirmed'
| 'permission_blocked'
| 'delivery_ready'
| 'member_ready'
| 'run_ready'
| 'run_failed'
| 'run_cancelled';
export interface OpenCodeLaunchCheckpoint {
name: OpenCodeLaunchCheckpointName;
teamName: string;
runId: string;
memberName: string | null;
runtimeSessionId: string | null;
hostKey: string | null;
evidenceHash: string;
createdAt: string;
diagnostics: string[];
}
export interface OpenCodeLaunchTransaction {
teamName: string;
runId: string;
providerId: 'opencode';
startedAt: string;
updatedAt: string;
status: 'active' | 'ready' | 'failed' | 'cancelled' | 'reconciled';
checkpoints: OpenCodeLaunchCheckpoint[];
}
export interface OpenCodeRunReadyInput {
members: Array<{ name: string; launchState?: string }>;
transaction: OpenCodeLaunchTransaction;
toolProof: { ok: boolean };
deliveryReady: boolean;
}
export class OpenCodeLaunchTransactionStore {
constructor(
private readonly store: VersionedJsonStore<OpenCodeLaunchTransaction[]>,
private readonly clock: () => Date = () => new Date()
) {}
async beginRun(input: {
teamName: string;
runId: string;
startedAt?: string;
}): Promise<
| { state: 'created'; transaction: OpenCodeLaunchTransaction }
| { state: 'already_active'; transaction: OpenCodeLaunchTransaction }
> {
let result:
| { state: 'created'; transaction: OpenCodeLaunchTransaction }
| { state: 'already_active'; transaction: OpenCodeLaunchTransaction }
| null = null;
const startedAt = input.startedAt ?? this.clock().toISOString();
await this.store.updateLocked((transactions) => {
const active = transactions.find(
(transaction) => transaction.teamName === input.teamName && transaction.status === 'active'
);
if (active) {
result = { state: 'already_active', transaction: active };
return transactions;
}
const transaction: OpenCodeLaunchTransaction = {
teamName: input.teamName,
runId: input.runId,
providerId: 'opencode',
startedAt,
updatedAt: startedAt,
status: 'active',
checkpoints: [],
};
result = { state: 'created', transaction };
return [...transactions, transaction];
});
if (!result) {
throw new Error('OpenCode launch transaction begin failed');
}
return result;
}
async addCheckpoint(input: OpenCodeLaunchCheckpoint): Promise<'created' | 'unchanged'> {
let outcome: 'created' | 'unchanged' = 'created';
await this.store.updateLocked((transactions) =>
transactions.map((transaction) => {
if (transaction.teamName !== input.teamName || transaction.runId !== input.runId) {
return transaction;
}
if (transaction.status !== 'active') {
throw new Error(`OpenCode launch transaction is not active: ${input.runId}`);
}
const duplicate = transaction.checkpoints.some(
(checkpoint) =>
checkpoint.name === input.name &&
checkpoint.memberName === input.memberName &&
checkpoint.evidenceHash === input.evidenceHash
);
if (duplicate) {
outcome = 'unchanged';
return transaction;
}
return {
...transaction,
updatedAt: input.createdAt,
checkpoints: [...transaction.checkpoints, normalizeCheckpoint(input)],
};
})
);
if (!(await this.hasTransaction(input.teamName, input.runId))) {
throw new Error(`OpenCode launch transaction not found: ${input.runId}`);
}
return outcome;
}
async hasCheckpoint(input: {
teamName: string;
runId: string;
memberName: string | null;
name: OpenCodeLaunchCheckpointName;
evidenceHash?: string;
}): Promise<boolean> {
const transaction = await this.read(input.teamName, input.runId);
return (
transaction?.checkpoints.some(
(checkpoint) =>
checkpoint.name === input.name &&
checkpoint.memberName === input.memberName &&
(input.evidenceHash === undefined || checkpoint.evidenceHash === input.evidenceHash)
) ?? false
);
}
async readActive(teamName: string): Promise<OpenCodeLaunchTransaction | null> {
const transactions = await this.readRequired();
return (
transactions.find(
(transaction) => transaction.teamName === teamName && transaction.status === 'active'
) ?? null
);
}
async read(teamName: string, runId: string): Promise<OpenCodeLaunchTransaction | null> {
const transactions = await this.readRequired();
return (
transactions.find(
(transaction) => transaction.teamName === teamName && transaction.runId === runId
) ?? null
);
}
async finish(input: {
teamName: string;
runId: string;
status: 'ready' | 'failed' | 'cancelled' | 'reconciled';
updatedAt?: string;
}): Promise<'finished' | 'unchanged'> {
let found = false;
let outcome: 'finished' | 'unchanged' = 'finished';
const updatedAt = input.updatedAt ?? this.clock().toISOString();
await this.store.updateLocked((transactions) =>
transactions.map((transaction) => {
if (transaction.teamName !== input.teamName || transaction.runId !== input.runId) {
return transaction;
}
found = true;
if (transaction.status !== 'active') {
outcome = 'unchanged';
return transaction;
}
return {
...transaction,
status: input.status,
updatedAt,
};
})
);
if (!found) {
const active = await this.readActive(input.teamName);
if (active) {
throw new Error(
`OpenCode launch transaction ${input.runId} is stale; active run is ${active.runId}`
);
}
throw new Error(`OpenCode launch transaction not found: ${input.runId}`);
}
return outcome;
}
async list(): Promise<OpenCodeLaunchTransaction[]> {
return this.readRequired();
}
private async hasTransaction(teamName: string, runId: string): Promise<boolean> {
return (await this.read(teamName, runId)) !== null;
}
private async readRequired(): Promise<OpenCodeLaunchTransaction[]> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
return result.data;
}
}
export function canMarkOpenCodeRunReady(input: OpenCodeRunReadyInput): {
ok: boolean;
missing: string[];
} {
const missing: string[] = [];
for (const member of input.members) {
if (!hasMemberCheckpoint(input.transaction, member.name, 'member_session_recorded')) {
missing.push(`${member.name}:member_session_recorded`);
}
if (!hasMemberCheckpoint(input.transaction, member.name, 'required_tools_proven')) {
missing.push(`${member.name}:required_tools_proven`);
}
if (member.launchState !== 'confirmed_alive') {
missing.push(`${member.name}:bootstrap_confirmed`);
}
}
if (!input.toolProof.ok) {
missing.push('required_runtime_tools');
}
if (!input.deliveryReady) {
missing.push('runtime_delivery_service');
}
return {
ok: missing.length === 0,
missing,
};
}
export function hasMemberCheckpoint(
transaction: OpenCodeLaunchTransaction,
memberName: string,
name: OpenCodeLaunchCheckpointName
): boolean {
return transaction.checkpoints.some(
(checkpoint) => checkpoint.memberName === memberName && checkpoint.name === name
);
}
export function createOpenCodeLaunchEvidenceHash(evidence: unknown): string {
return `sha256:${createHash('sha256')
.update(stableJsonStringify(redactOpenCodeLaunchEvidence(evidence)))
.digest('hex')}`;
}
export function redactOpenCodeLaunchEvidence(evidence: unknown): unknown {
if (evidence === null || typeof evidence !== 'object') {
return evidence;
}
if (Array.isArray(evidence)) {
return evidence.map(redactOpenCodeLaunchEvidence);
}
const output: Record<string, unknown> = {};
for (const [key, value] of Object.entries(evidence)) {
if (/token|secret|password|api[_-]?key|authorization/i.test(key)) {
output[key] = '[redacted]';
} else {
output[key] = redactOpenCodeLaunchEvidence(value);
}
}
return output;
}
export function createOpenCodeLaunchTransactionStore(options: {
filePath: string;
clock?: () => Date;
}): OpenCodeLaunchTransactionStore {
const clock = options.clock ?? (() => new Date());
return new OpenCodeLaunchTransactionStore(
new VersionedJsonStore<OpenCodeLaunchTransaction[]>({
filePath: options.filePath,
schemaVersion: OPENCODE_LAUNCH_TRANSACTION_SCHEMA_VERSION,
defaultData: () => [],
validate: validateOpenCodeLaunchTransactions,
clock,
}),
clock
);
}
export function validateOpenCodeLaunchTransactions(value: unknown): OpenCodeLaunchTransaction[] {
if (!Array.isArray(value)) {
throw new Error('OpenCode launch transactions must be an array');
}
return value.map((transaction, index) => {
if (!isLaunchTransaction(transaction)) {
throw new Error(`Invalid OpenCode launch transaction at index ${index}`);
}
return transaction;
});
}
function normalizeCheckpoint(input: OpenCodeLaunchCheckpoint): OpenCodeLaunchCheckpoint {
return {
...input,
diagnostics: [...input.diagnostics],
};
}
function isLaunchTransaction(value: unknown): value is OpenCodeLaunchTransaction {
return (
isRecord(value) &&
isNonEmptyString(value.teamName) &&
isNonEmptyString(value.runId) &&
value.providerId === 'opencode' &&
isNonEmptyString(value.startedAt) &&
isNonEmptyString(value.updatedAt) &&
(value.status === 'active' ||
value.status === 'ready' ||
value.status === 'failed' ||
value.status === 'cancelled' ||
value.status === 'reconciled') &&
Array.isArray(value.checkpoints) &&
value.checkpoints.every(isLaunchCheckpoint)
);
}
function isLaunchCheckpoint(value: unknown): value is OpenCodeLaunchCheckpoint {
return (
isRecord(value) &&
isLaunchCheckpointName(value.name) &&
isNonEmptyString(value.teamName) &&
isNonEmptyString(value.runId) &&
isNullableString(value.memberName) &&
isNullableString(value.runtimeSessionId) &&
isNullableString(value.hostKey) &&
isNonEmptyString(value.evidenceHash) &&
isNonEmptyString(value.createdAt) &&
Array.isArray(value.diagnostics) &&
value.diagnostics.every((item) => typeof item === 'string')
);
}
function isLaunchCheckpointName(value: unknown): value is OpenCodeLaunchCheckpointName {
return (
value === 'run_created' ||
value === 'host_ready' ||
value === 'lead_session_recorded' ||
value === 'member_session_recorded' ||
value === 'mcp_connected' ||
value === 'required_tools_proven' ||
value === 'prompt_sent' ||
value === 'bootstrap_confirmed' ||
value === 'permission_blocked' ||
value === 'delivery_ready' ||
value === 'member_ready' ||
value === 'run_ready' ||
value === 'run_failed' ||
value === 'run_cancelled'
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function isNullableString(value: unknown): value is string | null {
return value === null || typeof value === 'string';
}

View file

@ -0,0 +1,48 @@
import * as path from 'path';
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
import { createRuntimeStoreManifestStore } from './RuntimeStoreManifest';
export interface OpenCodeRuntimeManifestEvidenceReaderOptions {
teamsBasePath: string;
clock?: () => Date;
}
const OPENCODE_TEAM_RUNTIME_DIR = '.opencode-runtime';
const OPENCODE_RUNTIME_MANIFEST_FILE = 'manifest.json';
export class OpenCodeRuntimeManifestEvidenceReader implements RuntimeStoreManifestReader {
private readonly teamsBasePath: string;
private readonly clock: () => Date;
constructor(options: OpenCodeRuntimeManifestEvidenceReaderOptions) {
this.teamsBasePath = options.teamsBasePath;
this.clock = options.clock ?? (() => new Date());
}
async read(teamName: string): Promise<RuntimeStoreManifestEvidence> {
const manifest = await createRuntimeStoreManifestStore({
filePath: getOpenCodeRuntimeManifestPath(this.teamsBasePath, teamName),
teamName,
clock: this.clock,
}).read();
return {
highWatermark: manifest.highWatermark,
activeRunId: manifest.activeRunId,
capabilitySnapshotId: manifest.activeCapabilitySnapshotId,
};
}
}
export function getOpenCodeTeamRuntimeDirectory(teamsBasePath: string, teamName: string): string {
return path.join(teamsBasePath, teamName, OPENCODE_TEAM_RUNTIME_DIR);
}
export function getOpenCodeRuntimeManifestPath(teamsBasePath: string, teamName: string): string {
return path.join(
getOpenCodeTeamRuntimeDirectory(teamsBasePath, teamName),
OPENCODE_RUNTIME_MANIFEST_FILE
);
}

View file

@ -0,0 +1,313 @@
import { randomUUID } from 'crypto';
import { VersionedJsonStore, VersionedJsonStoreError } from './VersionedJsonStore';
export const OPENCODE_RUNTIME_RUN_TOMBSTONE_SCHEMA_VERSION = 1;
export type RuntimeEvidenceKind =
| 'sse_event'
| 'permission_reply'
| 'delivery_call'
| 'prompt_error'
| 'bootstrap_checkin'
| 'launch_checkpoint'
| 'heartbeat'
| 'bridge_result'
| 'recovery_result';
export type RuntimeRunTombstoneReason =
| 'stop_requested'
| 'relaunch_started'
| 'run_replaced'
| 'provider_session_aborted'
| 'recovery_rejected';
export interface RuntimeRunTombstone {
tombstoneId: string;
teamName: string;
runId: string;
reason: RuntimeRunTombstoneReason;
evidenceKinds: RuntimeEvidenceKind[];
createdAt: string;
expiresAt: string | null;
diagnostic: string | null;
}
export interface RuntimeEvidenceAcceptanceInput {
teamName: string;
runId: string | null;
currentRunId: string | null;
evidenceKind: RuntimeEvidenceKind;
}
export class RuntimeStaleEvidenceError extends Error {
constructor(
message: string,
readonly reason: 'missing_run_id' | 'current_run_missing' | 'run_mismatch' | 'run_tombstoned',
readonly evidenceKind: RuntimeEvidenceKind,
readonly runId: string | null
) {
super(message);
this.name = 'RuntimeStaleEvidenceError';
}
}
export class RuntimeRunTombstoneStore {
constructor(
private readonly store: VersionedJsonStore<RuntimeRunTombstone[]>,
private readonly options: {
idFactory?: () => string;
clock?: () => Date;
} = {}
) {}
async add(input: {
teamName: string;
runId: string;
reason: RuntimeRunTombstoneReason;
evidenceKinds?: RuntimeEvidenceKind[];
ttlMs?: number;
diagnostic?: string | null;
}): Promise<RuntimeRunTombstone> {
const clock = this.options.clock ?? (() => new Date());
const now = clock();
let created: RuntimeRunTombstone | null = null;
await this.store.updateLocked((records) => {
const compacted = compactRuntimeRunTombstones(records, now);
const existing = compacted.find(
(record) =>
record.teamName === input.teamName &&
record.runId === input.runId &&
record.reason === input.reason
);
if (existing) {
created = existing;
return compacted;
}
created = {
tombstoneId: this.options.idFactory?.() ?? `opencode-run-tombstone-${randomUUID()}`,
teamName: input.teamName,
runId: input.runId,
reason: input.reason,
evidenceKinds: normalizeEvidenceKinds(input.evidenceKinds),
createdAt: now.toISOString(),
expiresAt:
typeof input.ttlMs === 'number'
? new Date(now.getTime() + input.ttlMs).toISOString()
: null,
diagnostic: input.diagnostic ?? null,
};
return [...compacted, created];
});
if (!created) {
throw new Error('Runtime run tombstone was not created');
}
return created;
}
async list(teamName: string): Promise<RuntimeRunTombstone[]> {
const records = await this.readRequired();
const now = (this.options.clock ?? (() => new Date()))();
return compactRuntimeRunTombstones(records, now).filter(
(record) => record.teamName === teamName
);
}
async find(input: {
teamName: string;
runId: string;
evidenceKind?: RuntimeEvidenceKind;
}): Promise<RuntimeRunTombstone | null> {
const records = await this.list(input.teamName);
return (
records.find(
(record) =>
record.runId === input.runId &&
(!input.evidenceKind || record.evidenceKinds.includes(input.evidenceKind))
) ?? null
);
}
async assertEvidenceAccepted(input: RuntimeEvidenceAcceptanceInput): Promise<void> {
assertRuntimeEvidenceRunMatches(input);
const tombstone = input.runId
? await this.find({
teamName: input.teamName,
runId: input.runId,
evidenceKind: input.evidenceKind,
})
: null;
if (tombstone) {
throw new RuntimeStaleEvidenceError(
`Rejected stale runtime evidence: ${input.evidenceKind}`,
'run_tombstoned',
input.evidenceKind,
input.runId
);
}
}
async compact(): Promise<number> {
const now = (this.options.clock ?? (() => new Date()))();
let removed = 0;
await this.store.updateLocked((records) => {
const compacted = compactRuntimeRunTombstones(records, now);
removed = records.length - compacted.length;
return compacted;
});
return removed;
}
private async readRequired(): Promise<RuntimeRunTombstone[]> {
const result = await this.store.read();
if (!result.ok) {
throw new VersionedJsonStoreError(result.message, result.reason, result.quarantinePath);
}
return result.data;
}
}
export function assertRuntimeEvidenceRunMatches(input: RuntimeEvidenceAcceptanceInput): void {
if (!input.runId) {
throw new RuntimeStaleEvidenceError(
`Rejected runtime evidence without run id: ${input.evidenceKind}`,
'missing_run_id',
input.evidenceKind,
input.runId
);
}
if (!input.currentRunId) {
throw new RuntimeStaleEvidenceError(
`Rejected runtime evidence without current run: ${input.evidenceKind}`,
'current_run_missing',
input.evidenceKind,
input.runId
);
}
if (input.runId !== input.currentRunId) {
throw new RuntimeStaleEvidenceError(
`Rejected stale runtime evidence: ${input.evidenceKind}`,
'run_mismatch',
input.evidenceKind,
input.runId
);
}
}
export function createRuntimeRunTombstoneStore(options: {
filePath: string;
idFactory?: () => string;
clock?: () => Date;
}): RuntimeRunTombstoneStore {
const clock = options.clock ?? (() => new Date());
return new RuntimeRunTombstoneStore(
new VersionedJsonStore<RuntimeRunTombstone[]>({
filePath: options.filePath,
schemaVersion: OPENCODE_RUNTIME_RUN_TOMBSTONE_SCHEMA_VERSION,
defaultData: () => [],
validate: validateRuntimeRunTombstones,
clock,
}),
{
idFactory: options.idFactory,
clock,
}
);
}
export function validateRuntimeRunTombstones(value: unknown): RuntimeRunTombstone[] {
if (!Array.isArray(value)) {
throw new Error('Runtime run tombstones must be an array');
}
const seen = new Set<string>();
return value.map((record, index) => {
if (!isRuntimeRunTombstone(record)) {
throw new Error(`Invalid runtime run tombstone at index ${index}`);
}
if (seen.has(record.tombstoneId)) {
throw new Error(`Duplicate runtime run tombstone id: ${record.tombstoneId}`);
}
seen.add(record.tombstoneId);
return record;
});
}
export function compactRuntimeRunTombstones(
records: RuntimeRunTombstone[],
now: Date
): RuntimeRunTombstone[] {
const nowMs = now.getTime();
return records.filter(
(record) => record.expiresAt === null || Date.parse(record.expiresAt) > nowMs
);
}
function normalizeEvidenceKinds(input: RuntimeEvidenceKind[] | undefined): RuntimeEvidenceKind[] {
const all: RuntimeEvidenceKind[] = [
'sse_event',
'permission_reply',
'delivery_call',
'prompt_error',
'bootstrap_checkin',
'launch_checkpoint',
'heartbeat',
'bridge_result',
'recovery_result',
];
const source = input && input.length > 0 ? input : all;
return [...new Set(source)].sort();
}
function isRuntimeRunTombstone(value: unknown): value is RuntimeRunTombstone {
return (
isRecord(value) &&
isNonEmptyString(value.tombstoneId) &&
isNonEmptyString(value.teamName) &&
isNonEmptyString(value.runId) &&
isRuntimeRunTombstoneReason(value.reason) &&
Array.isArray(value.evidenceKinds) &&
value.evidenceKinds.length > 0 &&
value.evidenceKinds.every(isRuntimeEvidenceKind) &&
isNonEmptyString(value.createdAt) &&
(value.expiresAt === null || isNonEmptyString(value.expiresAt)) &&
(value.diagnostic === null || typeof value.diagnostic === 'string')
);
}
function isRuntimeRunTombstoneReason(value: unknown): value is RuntimeRunTombstoneReason {
return (
value === 'stop_requested' ||
value === 'relaunch_started' ||
value === 'run_replaced' ||
value === 'provider_session_aborted' ||
value === 'recovery_rejected'
);
}
function isRuntimeEvidenceKind(value: unknown): value is RuntimeEvidenceKind {
return (
value === 'sse_event' ||
value === 'permission_reply' ||
value === 'delivery_call' ||
value === 'prompt_error' ||
value === 'bootstrap_checkin' ||
value === 'launch_checkpoint' ||
value === 'heartbeat' ||
value === 'bridge_result' ||
value === 'recovery_result'
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,292 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { withFileLock } from '../../fileLock';
export interface VersionedJsonStoreEnvelope<TData> {
schemaVersion: number;
updatedAt: string;
data: TData;
}
export type VersionedJsonStoreReadStatus = 'missing' | 'loaded';
export type VersionedJsonStoreFailureReason =
| 'invalid_json'
| 'invalid_envelope'
| 'invalid_data'
| 'future_schema';
export type VersionedJsonStoreReadResult<TData> =
| {
ok: true;
status: VersionedJsonStoreReadStatus;
data: TData;
envelope: VersionedJsonStoreEnvelope<TData> | null;
}
| {
ok: false;
reason: VersionedJsonStoreFailureReason;
message: string;
quarantinePath: string | null;
};
export interface VersionedJsonStoreUpdateResult<TData> {
changed: boolean;
data: TData;
envelope: VersionedJsonStoreEnvelope<TData>;
}
export interface VersionedJsonStoreOptions<TData> {
filePath: string;
schemaVersion: number;
defaultData: () => TData;
validate: (value: unknown) => TData;
clock?: () => Date;
quarantineDir?: string;
}
export class VersionedJsonStoreError extends Error {
constructor(
message: string,
readonly reason: VersionedJsonStoreFailureReason,
readonly quarantinePath: string | null
) {
super(message);
this.name = 'VersionedJsonStoreError';
}
}
export class VersionedJsonStore<TData> {
private readonly filePath: string;
private readonly schemaVersion: number;
private readonly defaultData: () => TData;
private readonly validate: (value: unknown) => TData;
private readonly clock: () => Date;
private readonly quarantineDir: string | null;
constructor(options: VersionedJsonStoreOptions<TData>) {
this.filePath = options.filePath;
this.schemaVersion = options.schemaVersion;
this.defaultData = options.defaultData;
this.validate = options.validate;
this.clock = options.clock ?? (() => new Date());
this.quarantineDir = options.quarantineDir ?? null;
}
async read(): Promise<VersionedJsonStoreReadResult<TData>> {
return this.readUnlocked();
}
async updateLocked(
updater: (current: TData) => TData | Promise<TData>
): Promise<VersionedJsonStoreUpdateResult<TData>> {
return withFileLock(this.filePath, async () => {
const current = await this.readUnlocked();
if (!current.ok) {
throw new VersionedJsonStoreError(current.message, current.reason, current.quarantinePath);
}
const nextData = await updater(cloneJson(current.data));
const validatedNextData = this.validate(nextData);
const currentJson = stableJsonStringify(current.data);
const nextJson = stableJsonStringify(validatedNextData);
const changed = current.status === 'missing' || currentJson !== nextJson;
const envelope: VersionedJsonStoreEnvelope<TData> = {
schemaVersion: this.schemaVersion,
updatedAt: changed
? this.clock().toISOString()
: (current.envelope?.updatedAt ?? this.clock().toISOString()),
data: changed ? validatedNextData : current.data,
};
if (changed) {
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
await atomicWriteAsync(this.filePath, `${JSON.stringify(envelope, null, 2)}\n`);
}
return {
changed,
data: envelope.data,
envelope,
};
});
}
private async readUnlocked(): Promise<VersionedJsonStoreReadResult<TData>> {
let raw: string;
try {
raw = await fs.readFile(this.filePath, 'utf8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
const data = this.validate(this.defaultData());
return {
ok: true,
status: 'missing',
data,
envelope: null,
};
}
throw error;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (error) {
const quarantinePath = await this.quarantine(raw, 'invalid_json');
return {
ok: false,
reason: 'invalid_json',
message: `Invalid JSON in versioned store ${this.filePath}: ${stringifyError(error)}`,
quarantinePath,
};
}
const envelopeResult = this.normalizeEnvelope(parsed);
if (!envelopeResult.ok) {
const quarantinePath = await this.quarantine(raw, envelopeResult.reason);
return {
ok: false,
reason: envelopeResult.reason,
message: envelopeResult.message,
quarantinePath,
};
}
if (envelopeResult.envelope.schemaVersion > this.schemaVersion) {
const quarantinePath = await this.quarantine(raw, 'future_schema');
return {
ok: false,
reason: 'future_schema',
message: `Future schema ${envelopeResult.envelope.schemaVersion} in ${this.filePath}; supported ${this.schemaVersion}`,
quarantinePath,
};
}
try {
const data = this.validate(envelopeResult.envelope.data);
return {
ok: true,
status: 'loaded',
data,
envelope: {
schemaVersion: envelopeResult.envelope.schemaVersion,
updatedAt: envelopeResult.envelope.updatedAt,
data,
},
};
} catch (error) {
const quarantinePath = await this.quarantine(raw, 'invalid_data');
return {
ok: false,
reason: 'invalid_data',
message: `Invalid data in versioned store ${this.filePath}: ${stringifyError(error)}`,
quarantinePath,
};
}
}
private normalizeEnvelope(
value: unknown
):
| { ok: true; envelope: VersionedJsonStoreEnvelope<unknown> }
| { ok: false; reason: VersionedJsonStoreFailureReason; message: string } {
if (!isRecord(value)) {
return {
ok: false,
reason: 'invalid_envelope',
message: `Versioned store ${this.filePath} must contain a JSON object`,
};
}
const schemaVersion = value.schemaVersion;
if (!Number.isInteger(schemaVersion) || (schemaVersion as number) < 1) {
return {
ok: false,
reason: 'invalid_envelope',
message: `Versioned store ${this.filePath} has invalid schemaVersion`,
};
}
if (typeof value.updatedAt !== 'string' || !value.updatedAt.trim()) {
return {
ok: false,
reason: 'invalid_envelope',
message: `Versioned store ${this.filePath} has invalid updatedAt`,
};
}
if (!Object.prototype.hasOwnProperty.call(value, 'data')) {
return {
ok: false,
reason: 'invalid_envelope',
message: `Versioned store ${this.filePath} is missing data`,
};
}
return {
ok: true,
envelope: {
schemaVersion: schemaVersion as number,
updatedAt: value.updatedAt,
data: value.data,
},
};
}
private async quarantine(
raw: string,
reason: VersionedJsonStoreFailureReason
): Promise<string | null> {
const dir = this.quarantineDir ?? path.dirname(this.filePath);
const baseName = path.basename(this.filePath);
const stamp = this.clock().toISOString().replace(/[:.]/g, '-');
const quarantinePath = path.join(dir, `${baseName}.${reason}.${stamp}.quarantine`);
try {
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(quarantinePath, raw, 'utf8');
return quarantinePath;
} catch {
return null;
}
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function cloneJson<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function stableJsonStringify(value: unknown): string {
return JSON.stringify(normalizeStableJson(value));
}
function normalizeStableJson(value: unknown): unknown {
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
return value.map(normalizeStableJson);
}
const output: Record<string, unknown> = {};
for (const key of Object.keys(value).sort()) {
const nested = (value as Record<string, unknown>)[key];
if (nested !== undefined) {
output[key] = normalizeStableJson(nested);
}
}
return output;
}
function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,284 @@
import { createHash } from 'crypto';
import { promises as fs } from 'fs';
import type {
OpenCodeApiCapabilities,
OpenCodeApiEndpointKey,
OpenCodeEndpointEvidence,
} from '../capabilities/OpenCodeApiCapabilities';
import {
assertOpenCodeProductionE2EEvidenceBasics,
type OpenCodeProductionE2EEvidence,
} from '../e2e/OpenCodeProductionE2EEvidence';
export interface OpenCodeSupportedVersionPolicy {
minimumVersion: string;
testedVersion: string;
allowedPrerelease: boolean;
requireCapabilities: boolean;
requireE2EArtifactsForTestedVersion: boolean;
}
export const OPENCODE_TEAM_LAUNCH_VERSION_POLICY: OpenCodeSupportedVersionPolicy = {
minimumVersion: '1.14.19',
testedVersion: '1.14.19',
allowedPrerelease: false,
requireCapabilities: true,
requireE2EArtifactsForTestedVersion: true,
};
export type OpenCodeInstallMethod = 'brew' | 'npm' | 'bun' | 'manual' | 'unknown';
export interface OpenCodeSemver {
major: number;
minor: number;
patch: number;
prerelease: string[];
}
export type OpenCodeSupportLevel =
| 'unsupported_too_old'
| 'unsupported_prerelease'
| 'supported_capabilities_pending'
| 'supported_e2e_pending'
| 'production_supported';
export { type OpenCodeProductionE2EEvidence } from '../e2e/OpenCodeProductionE2EEvidence';
export interface OpenCodeCompatibilitySnapshot {
schemaVersion: 1;
createdAt: string;
binaryPath: string;
binaryFingerprint: string;
installMethod: OpenCodeInstallMethod;
version: string;
semver: OpenCodeSemver;
supported: boolean;
supportLevel: OpenCodeSupportLevel;
apiCapabilities: OpenCodeApiCapabilities;
testedEvidencePath: string | null;
diagnostics: string[];
}
export interface OpenCodeSupportDecision {
supported: boolean;
supportLevel: OpenCodeSupportLevel;
semver: OpenCodeSemver | null;
diagnostics: string[];
}
export interface OpenCodeRouteCompatibilityCache {
binaryFingerprint: string;
version: string;
routes: Record<
OpenCodeApiEndpointKey,
{
available: boolean;
evidence: OpenCodeEndpointEvidence;
lastVerifiedAt: string;
}
>;
}
export type OpenCodePermissionReplyRoute =
| {
kind: 'primary_permission_reply';
method: 'POST';
pathTemplate: '/permission/:requestID/reply';
bodyShape: { reply: 'once' };
}
| {
kind: 'deprecated_session_permission';
method: 'POST';
pathTemplate: '/session/:sessionID/permissions/:permissionID';
bodyShape: { response: 'once' };
};
export async function buildOpenCodeBinaryFingerprint(binaryPath: string): Promise<string> {
const stat = await fs.stat(binaryPath);
return stableHash({
binaryPath,
realPath: await fs.realpath(binaryPath),
size: stat.size,
mtimeMs: stat.mtimeMs,
});
}
export function shouldReuseCompatibilitySnapshot(input: {
cached: OpenCodeCompatibilitySnapshot | null;
binaryPath: string;
binaryFingerprint: string;
version: string;
}): boolean {
return Boolean(
input.cached &&
input.cached.binaryPath === input.binaryPath &&
input.cached.binaryFingerprint === input.binaryFingerprint &&
input.cached.version === input.version
);
}
export function evaluateOpenCodeSupport(input: {
version: string;
capabilities: OpenCodeApiCapabilities;
evidence: OpenCodeProductionE2EEvidence | null;
policy?: OpenCodeSupportedVersionPolicy;
}): OpenCodeSupportDecision {
const policy = input.policy ?? OPENCODE_TEAM_LAUNCH_VERSION_POLICY;
const parsed = parseOpenCodeSemver(input.version);
if (!parsed || semverCoreLt(parsed, policy.minimumVersion)) {
return {
supported: false,
supportLevel: 'unsupported_too_old',
semver: parsed,
diagnostics: [
`OpenCode ${input.version} is below supported minimum ${policy.minimumVersion}`,
],
};
}
if (parsed.prerelease.length > 0 && !policy.allowedPrerelease) {
return {
supported: false,
supportLevel: 'unsupported_prerelease',
semver: parsed,
diagnostics: [
`OpenCode prerelease ${input.version} is not enabled for production team launch`,
],
};
}
if (policy.requireCapabilities && !input.capabilities.requiredForTeamLaunch.ready) {
return {
supported: false,
supportLevel: 'supported_capabilities_pending',
semver: parsed,
diagnostics: input.capabilities.requiredForTeamLaunch.missing,
};
}
if (policy.requireE2EArtifactsForTestedVersion) {
const evidenceDecision = assertOpenCodeProductionE2EGate({
evidence: input.evidence,
testedVersion: policy.testedVersion,
});
if (!evidenceDecision.ok) {
return {
supported: false,
supportLevel: 'supported_e2e_pending',
semver: parsed,
diagnostics: evidenceDecision.diagnostics,
};
}
}
return {
supported: true,
supportLevel: 'production_supported',
semver: parsed,
diagnostics: [],
};
}
export function assertOpenCodeProductionE2EGate(input: {
evidence: OpenCodeProductionE2EEvidence | null;
testedVersion: string;
now?: Date;
}): { ok: boolean; diagnostics: string[] } {
return assertOpenCodeProductionE2EEvidenceBasics(input);
}
export function selectPermissionReplyRouteFromCache(
cache: OpenCodeRouteCompatibilityCache
): OpenCodePermissionReplyRoute | null {
if (cache.routes.permissionReply?.available) {
return {
kind: 'primary_permission_reply',
method: 'POST',
pathTemplate: '/permission/:requestID/reply',
bodyShape: { reply: 'once' },
};
}
if (cache.routes.permissionLegacySessionRespond?.available) {
return {
kind: 'deprecated_session_permission',
method: 'POST',
pathTemplate: '/session/:sessionID/permissions/:permissionID',
bodyShape: { response: 'once' },
};
}
return null;
}
export function parseOpenCodeSemver(version: string): OpenCodeSemver | null {
const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
if (!match) {
return null;
}
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
prerelease: match[4]?.split('.').filter(Boolean) ?? [],
};
}
export function semverLt(left: OpenCodeSemver, right: string | OpenCodeSemver): boolean {
const parsedRight = typeof right === 'string' ? parseOpenCodeSemver(right) : right;
if (!parsedRight) {
return true;
}
for (const key of ['major', 'minor', 'patch'] as const) {
if (left[key] < parsedRight[key]) {
return true;
}
if (left[key] > parsedRight[key]) {
return false;
}
}
if (left.prerelease.length > 0 && parsedRight.prerelease.length === 0) {
return true;
}
return false;
}
function semverCoreLt(left: OpenCodeSemver, right: string | OpenCodeSemver): boolean {
const parsedRight = typeof right === 'string' ? parseOpenCodeSemver(right) : right;
if (!parsedRight) {
return true;
}
for (const key of ['major', 'minor', 'patch'] as const) {
if (left[key] < parsedRight[key]) {
return true;
}
if (left[key] > parsedRight[key]) {
return false;
}
}
return false;
}
function stableHash(value: unknown): string {
return createHash('sha256').update(stableJsonStringify(value)).digest('hex');
}
function stableJsonStringify(value: unknown): string {
if (value === null || typeof value !== 'object') {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(stableJsonStringify).join(',')}]`;
}
return `{${Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => `${JSON.stringify(key)}:${stableJsonStringify(item)}`)
.join(',')}}`;
}

View file

@ -0,0 +1,499 @@
import { randomUUID } from 'crypto';
import type { OpenCodeTeamLaunchReadiness } from '../opencode/readiness/OpenCodeTeamLaunchReadiness';
import type {
OpenCodeLaunchTeamCommandBody,
OpenCodeLaunchTeamCommandData,
OpenCodeBridgeRuntimeSnapshot,
OpenCodeReconcileTeamCommandBody,
OpenCodeStopTeamCommandBody,
OpenCodeStopTeamCommandData,
OpenCodeTeamLaunchMode,
OpenCodeTeamMemberLaunchBridgeState,
} from '../opencode/bridge/OpenCodeBridgeCommandContract';
import type {
TeamLaunchRuntimeAdapter,
TeamRuntimeLaunchInput,
TeamRuntimeLaunchResult,
TeamRuntimeMemberLaunchEvidence,
TeamRuntimeMemberStopEvidence,
TeamRuntimePrepareResult,
TeamRuntimeReconcileInput,
TeamRuntimeReconcileResult,
TeamRuntimeStopInput,
TeamRuntimeStopResult,
} from './TeamRuntimeAdapter';
export interface OpenCodeTeamRuntimeBridgePort {
checkOpenCodeTeamLaunchReadiness(input: {
projectPath: string;
selectedModel: string | null;
requireExecutionProbe: boolean;
launchMode?: OpenCodeTeamLaunchMode;
}): Promise<OpenCodeTeamLaunchReadiness>;
getLastOpenCodeRuntimeSnapshot?(projectPath: string): OpenCodeBridgeRuntimeSnapshot | null;
launchOpenCodeTeam?(input: OpenCodeLaunchTeamCommandBody): Promise<OpenCodeLaunchTeamCommandData>;
reconcileOpenCodeTeam?(
input: OpenCodeReconcileTeamCommandBody
): Promise<OpenCodeLaunchTeamCommandData>;
stopOpenCodeTeam?(input: OpenCodeStopTeamCommandBody): Promise<OpenCodeStopTeamCommandData>;
}
export interface OpenCodeTeamRuntimeAdapterOptions {
launchMode?: OpenCodeTeamLaunchMode;
/**
* @deprecated Use launchMode. Kept for older tests/callers until the production gate is fully wired.
*/
launchEnabled?: boolean;
}
export { type OpenCodeTeamLaunchMode } from '../opencode/bridge/OpenCodeBridgeCommandContract';
const REQUIRED_READY_CHECKPOINTS = new Set([
'required_tools_proven',
'delivery_ready',
'member_ready',
'run_ready',
]);
export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
readonly providerId = 'opencode' as const;
private readonly lastProjectPathByTeamName = new Map<string, string>();
constructor(
private readonly bridge: OpenCodeTeamRuntimeBridgePort,
private readonly options: OpenCodeTeamRuntimeAdapterOptions = {}
) {}
async prepare(input: TeamRuntimeLaunchInput): Promise<TeamRuntimePrepareResult> {
const launchMode = resolveOpenCodeTeamLaunchMode(this.options);
if (launchMode === 'disabled') {
return {
ok: false,
providerId: this.providerId,
reason: 'opencode_team_launch_disabled',
retryable: false,
diagnostics: [
'OpenCode team launch mode is disabled. Set CLAUDE_TEAM_OPENCODE_LAUNCH_MODE=dogfood for local dogfood testing or production after strict readiness evidence exists.',
],
warnings: [],
};
}
const readiness = await this.bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: input.cwd,
selectedModel: input.model ?? null,
requireExecutionProbe: true,
launchMode,
});
if (!readiness.launchAllowed) {
return {
ok: false,
providerId: this.providerId,
reason: readiness.state,
retryable: isRetryableReadinessState(readiness.state),
diagnostics: mergeDiagnostics(readiness.diagnostics, readiness.missing),
warnings: [],
};
}
const warnings =
launchMode === 'dogfood'
? [
'OpenCode dogfood launch mode is active. This is local test mode and may run without production E2E evidence.',
]
: [];
if (launchMode === 'production' && readiness.supportLevel !== 'production_supported') {
return {
ok: false,
providerId: this.providerId,
reason: 'opencode_production_e2e_evidence_missing',
retryable: false,
diagnostics: [
'OpenCode production launch requires strict production E2E evidence before enabling team launch.',
],
warnings,
};
}
return {
ok: true,
providerId: this.providerId,
modelId: readiness.modelId,
diagnostics: readiness.diagnostics,
warnings,
};
}
async launch(input: TeamRuntimeLaunchInput): Promise<TeamRuntimeLaunchResult> {
const prepared = await this.prepare(input);
if (!prepared.ok) {
return blockedLaunchResult(input, prepared.reason, prepared.diagnostics, prepared.warnings);
}
if (!this.bridge.launchOpenCodeTeam) {
return blockedLaunchResult(input, 'opencode_launch_bridge_missing', [
'OpenCode readiness passed, but the state-changing launch bridge is not registered.',
]);
}
const selectedModel = prepared.modelId ?? input.model?.trim() ?? '';
if (!selectedModel) {
return blockedLaunchResult(input, 'opencode_model_unavailable', [
'OpenCode launch requires a selected raw model id.',
]);
}
const runtimeSnapshot = this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null;
this.lastProjectPathByTeamName.set(input.teamName, input.cwd);
const data = await this.bridge.launchOpenCodeTeam({
mode: resolveOpenCodeTeamLaunchMode(this.options),
runId: input.runId,
teamId: input.teamName,
teamName: input.teamName,
projectPath: input.cwd,
selectedModel,
members: input.expectedMembers.map((member) => ({
name: member.name,
role: member.role?.trim() || member.workflow?.trim() || 'teammate',
prompt: buildMemberBootstrapPrompt(input, member.name),
})),
leadPrompt: input.prompt?.trim() ?? '',
expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
});
return mapOpenCodeLaunchDataToRuntimeResult(input, data, prepared.warnings);
}
async reconcile(input: TeamRuntimeReconcileInput): Promise<TeamRuntimeReconcileResult> {
if (this.bridge.reconcileOpenCodeTeam) {
const projectPath =
input.expectedMembers[0]?.cwd ?? this.lastProjectPathByTeamName.get(input.teamName);
const runtimeSnapshot = projectPath
? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null)
: null;
const data = await this.bridge.reconcileOpenCodeTeam({
runId: input.runId,
teamId: input.teamName,
teamName: input.teamName,
projectPath,
expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
reconcileAttemptId: `opencode-reconcile-${randomUUID()}`,
expectedMembers: input.expectedMembers.map((member) => ({
name: member.name,
model: member.model ?? null,
})),
reason: input.reason,
});
const mapped = mapOpenCodeLaunchDataToRuntimeResult(
{
runId: input.runId,
teamName: input.teamName,
cwd: input.expectedMembers[0]?.cwd ?? '',
providerId: this.providerId,
skipPermissions: false,
expectedMembers: input.expectedMembers,
previousLaunchState: input.previousLaunchState,
},
data,
[]
);
return {
...mapped,
snapshot: input.previousLaunchState,
};
}
const snapshot = input.previousLaunchState;
if (!snapshot) {
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: 'reconciled',
teamLaunchState: 'partial_pending',
members: {},
snapshot: null,
warnings: [],
diagnostics: ['No previous OpenCode launch snapshot was available for reconciliation.'],
};
}
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: snapshot.launchPhase,
teamLaunchState: snapshot.teamLaunchState,
members: Object.fromEntries(
Object.entries(snapshot.members).map(([memberName, member]) => [
memberName,
{
memberName,
providerId: this.providerId,
launchState: member.launchState,
agentToolAccepted: member.agentToolAccepted,
runtimeAlive: member.runtimeAlive,
bootstrapConfirmed: member.bootstrapConfirmed,
hardFailure: member.hardFailure,
hardFailureReason: member.hardFailureReason,
diagnostics: member.diagnostics ?? [],
} satisfies TeamRuntimeMemberLaunchEvidence,
])
),
snapshot,
warnings: [],
diagnostics: [`OpenCode launch snapshot reconciled from ${input.reason}.`],
};
}
async stop(input: TeamRuntimeStopInput): Promise<TeamRuntimeStopResult> {
if (this.bridge.stopOpenCodeTeam) {
const projectPath = input.cwd ?? this.lastProjectPathByTeamName.get(input.teamName);
const runtimeSnapshot = projectPath
? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null)
: null;
const data = await this.bridge.stopOpenCodeTeam({
runId: input.runId,
teamId: input.teamName,
teamName: input.teamName,
projectPath,
expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
reason: input.reason,
force: input.force,
});
if (data.stopped) {
this.lastProjectPathByTeamName.delete(input.teamName);
}
return {
runId: input.runId,
teamName: input.teamName,
stopped: data.stopped,
members: Object.fromEntries(
Object.entries(data.members).map(([memberName, member]) => [
memberName,
{
memberName,
providerId: this.providerId,
stopped: member.stopped,
sessionId: member.sessionId,
diagnostics: member.diagnostics,
} satisfies TeamRuntimeMemberStopEvidence,
])
),
warnings: data.warnings.map((warning) => warning.message),
diagnostics: data.diagnostics.map(formatOpenCodeBridgeDiagnostic),
};
}
const members = input.previousLaunchState
? Object.fromEntries(
Object.keys(input.previousLaunchState.members).map((memberName) => [
memberName,
{
memberName,
providerId: this.providerId,
stopped: true,
diagnostics: [
'No live OpenCode session stop command is wired in this adapter shell.',
],
} satisfies TeamRuntimeMemberStopEvidence,
])
)
: {};
return {
runId: input.runId,
teamName: input.teamName,
stopped: true,
members,
warnings: [],
diagnostics: input.previousLaunchState
? ['OpenCode stop was acknowledged without live session ownership changes.']
: ['No previous OpenCode launch snapshot was available to stop.'],
};
}
}
export function resolveOpenCodeTeamLaunchMode(
options: OpenCodeTeamRuntimeAdapterOptions = {}
): OpenCodeTeamLaunchMode {
if (options.launchMode) {
return options.launchMode;
}
if (options.launchEnabled === true) {
return 'production';
}
return 'disabled';
}
function mapOpenCodeLaunchDataToRuntimeResult(
input: TeamRuntimeLaunchInput,
data: OpenCodeLaunchTeamCommandData,
prepareWarnings: string[]
): TeamRuntimeLaunchResult {
const checkpointNames = extractCheckpointNames(data);
const readyCheckpointsPresent = [...REQUIRED_READY_CHECKPOINTS].every((name) =>
checkpointNames.has(name)
);
const bridgeReady = data.teamLaunchState === 'ready';
const success = bridgeReady && readyCheckpointsPresent;
const checkpointDiagnostic = success
? []
: bridgeReady
? [
`OpenCode bridge reported ready without all required durable checkpoints: missing ${[
...REQUIRED_READY_CHECKPOINTS,
]
.filter((name) => !checkpointNames.has(name))
.join(', ')}`,
]
: [];
const members = Object.fromEntries(
input.expectedMembers.map((member) => {
const bridgeMember = data.members[member.name];
return [
member.name,
mapBridgeMemberToRuntimeEvidence(
member.name,
bridgeMember?.launchState ?? 'failed',
bridgeMember?.sessionId,
[
...(bridgeMember?.evidence ?? []).map(
(evidence) => `${evidence.kind} at ${evidence.observedAt}`
),
...checkpointDiagnostic,
]
),
];
})
);
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: success
? 'finished'
: data.teamLaunchState === 'launching'
? 'active'
: 'finished',
teamLaunchState: success
? 'clean_success'
: data.teamLaunchState === 'launching' || data.teamLaunchState === 'permission_blocked'
? 'partial_pending'
: 'partial_failure',
members,
warnings: [...prepareWarnings, ...data.warnings.map((warning) => warning.message)],
diagnostics: [...data.diagnostics.map(formatOpenCodeBridgeDiagnostic), ...checkpointDiagnostic],
};
}
function mapBridgeMemberToRuntimeEvidence(
memberName: string,
launchState: OpenCodeTeamMemberLaunchBridgeState,
sessionId: string | undefined,
diagnostics: string[]
): TeamRuntimeMemberLaunchEvidence {
const confirmed = launchState === 'confirmed_alive';
const createdOrBlocked = launchState === 'created' || launchState === 'permission_blocked';
const failed = launchState === 'failed';
return {
memberName,
providerId: 'opencode',
launchState: failed
? 'failed_to_start'
: confirmed
? 'confirmed_alive'
: 'runtime_pending_bootstrap',
agentToolAccepted: confirmed || createdOrBlocked,
runtimeAlive: confirmed || createdOrBlocked,
bootstrapConfirmed: confirmed,
hardFailure: failed,
hardFailureReason: failed ? 'OpenCode bridge reported member launch failure' : undefined,
sessionId,
diagnostics,
};
}
function extractCheckpointNames(data: OpenCodeLaunchTeamCommandData): Set<string> {
const names = new Set<string>();
for (const checkpoint of data.durableCheckpoints ?? []) {
if (checkpoint.name.trim()) names.add(checkpoint.name);
}
for (const member of Object.values(data.members)) {
for (const evidence of member.evidence) {
if (evidence.kind.trim()) names.add(evidence.kind);
}
}
return names;
}
function buildMemberBootstrapPrompt(input: TeamRuntimeLaunchInput, memberName: string): string {
const shared = input.prompt?.trim();
if (shared) {
return shared;
}
return `Join team "${input.teamName}" as "${memberName}" and wait for app MCP task delivery.`;
}
function formatOpenCodeBridgeDiagnostic(diagnostic: {
code: string;
severity: 'info' | 'warning' | 'error';
message: string;
}): string {
return `${diagnostic.severity}:${diagnostic.code}: ${diagnostic.message}`;
}
function blockedLaunchResult(
input: TeamRuntimeLaunchInput,
reason: string,
diagnostics: string[],
warnings: string[] = []
): TeamRuntimeLaunchResult {
const members = Object.fromEntries(
input.expectedMembers.map((member) => [
member.name,
{
memberName: member.name,
providerId: 'opencode' as const,
launchState: 'failed_to_start' as const,
agentToolAccepted: false,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: reason,
diagnostics,
},
])
);
return {
runId: input.runId,
teamName: input.teamName,
launchPhase: 'finished',
teamLaunchState: 'partial_failure',
members,
warnings,
diagnostics,
};
}
function isRetryableReadinessState(state: OpenCodeTeamLaunchReadiness['state']): boolean {
return (
state === 'not_installed' ||
state === 'not_authenticated' ||
state === 'e2e_missing' ||
state === 'runtime_store_blocked' ||
state === 'mcp_unavailable' ||
state === 'model_unavailable' ||
state === 'unknown_error'
);
}
function mergeDiagnostics(left: string[], right: string[]): string[] {
return [...new Set([...left, ...right].filter((value) => value.trim().length > 0))];
}

View file

@ -0,0 +1,184 @@
import type {
EffortLevel,
MemberLaunchState,
PersistedTeamLaunchPhase,
PersistedTeamLaunchSnapshot,
TeamAgentRuntimeBackendType,
TeamLaunchAggregateState,
} from '@shared/types';
export const TEAM_RUNTIME_PROVIDER_IDS = ['anthropic', 'codex', 'gemini', 'opencode'] as const;
export type TeamRuntimeProviderId = (typeof TEAM_RUNTIME_PROVIDER_IDS)[number];
export interface TeamRuntimeMemberSpec {
name: string;
role?: string;
workflow?: string;
providerId: TeamRuntimeProviderId;
model?: string;
effort?: EffortLevel;
cwd: string;
}
export interface TeamRuntimeLaunchInput {
runId: string;
teamName: string;
cwd: string;
prompt?: string;
providerId: TeamRuntimeProviderId;
model?: string;
effort?: EffortLevel;
skipPermissions: boolean;
expectedMembers: TeamRuntimeMemberSpec[];
previousLaunchState: PersistedTeamLaunchSnapshot | null;
}
export interface TeamRuntimePrepareSuccess {
ok: true;
providerId: TeamRuntimeProviderId;
modelId: string | null;
diagnostics: string[];
warnings: string[];
}
export interface TeamRuntimePrepareFailure {
ok: false;
providerId: TeamRuntimeProviderId;
reason: string;
diagnostics: string[];
warnings: string[];
retryable: boolean;
}
export type TeamRuntimePrepareResult = TeamRuntimePrepareSuccess | TeamRuntimePrepareFailure;
export interface TeamRuntimeMemberLaunchEvidence {
memberName: string;
providerId: TeamRuntimeProviderId;
launchState: MemberLaunchState;
agentToolAccepted: boolean;
runtimeAlive: boolean;
bootstrapConfirmed: boolean;
hardFailure: boolean;
hardFailureReason?: string;
sessionId?: string;
backendType?: TeamAgentRuntimeBackendType;
diagnostics: string[];
}
export interface TeamRuntimeLaunchResult {
runId: string;
teamName: string;
leadSessionId?: string;
launchPhase: PersistedTeamLaunchPhase;
teamLaunchState: TeamLaunchAggregateState;
members: Record<string, TeamRuntimeMemberLaunchEvidence>;
warnings: string[];
diagnostics: string[];
}
export type TeamRuntimeReconcileReason =
| 'startup_recovery'
| 'manual_refresh'
| 'launch_progress'
| 'provider_event'
| 'watcher_event'
| 'stop';
export interface TeamRuntimeReconcileInput {
runId: string;
teamName: string;
providerId: TeamRuntimeProviderId;
expectedMembers: TeamRuntimeMemberSpec[];
previousLaunchState: PersistedTeamLaunchSnapshot | null;
reason: TeamRuntimeReconcileReason;
}
export interface TeamRuntimeReconcileResult {
runId: string;
teamName: string;
launchPhase: PersistedTeamLaunchPhase;
teamLaunchState: TeamLaunchAggregateState;
members: Record<string, TeamRuntimeMemberLaunchEvidence>;
snapshot: PersistedTeamLaunchSnapshot | null;
warnings: string[];
diagnostics: string[];
}
export type TeamRuntimeStopReason = 'user_requested' | 'relaunch' | 'cleanup' | 'app_shutdown';
export interface TeamRuntimeStopInput {
runId: string;
teamName: string;
cwd?: string;
providerId: TeamRuntimeProviderId;
reason: TeamRuntimeStopReason;
previousLaunchState: PersistedTeamLaunchSnapshot | null;
force?: boolean;
}
export interface TeamRuntimeMemberStopEvidence {
memberName: string;
providerId: TeamRuntimeProviderId;
stopped: boolean;
sessionId?: string;
diagnostics: string[];
}
export interface TeamRuntimeStopResult {
runId: string;
teamName: string;
stopped: boolean;
members: Record<string, TeamRuntimeMemberStopEvidence>;
warnings: string[];
diagnostics: string[];
}
export interface TeamLaunchRuntimeAdapter {
readonly providerId: TeamRuntimeProviderId;
prepare(input: TeamRuntimeLaunchInput): Promise<TeamRuntimePrepareResult>;
launch(input: TeamRuntimeLaunchInput): Promise<TeamRuntimeLaunchResult>;
reconcile(input: TeamRuntimeReconcileInput): Promise<TeamRuntimeReconcileResult>;
stop(input: TeamRuntimeStopInput): Promise<TeamRuntimeStopResult>;
}
export function isTeamRuntimeProviderId(value: unknown): value is TeamRuntimeProviderId {
return value === 'anthropic' || value === 'codex' || value === 'gemini' || value === 'opencode';
}
export class TeamRuntimeAdapterRegistry {
private readonly adapters = new Map<TeamRuntimeProviderId, TeamLaunchRuntimeAdapter>();
constructor(adapters: readonly TeamLaunchRuntimeAdapter[] = []) {
for (const adapter of adapters) {
this.register(adapter);
}
}
register(adapter: TeamLaunchRuntimeAdapter): void {
if (!isTeamRuntimeProviderId(adapter.providerId)) {
throw new Error(`Invalid runtime adapter provider: ${String(adapter.providerId)}`);
}
if (this.adapters.has(adapter.providerId)) {
throw new Error(`Runtime adapter already registered: ${adapter.providerId}`);
}
this.adapters.set(adapter.providerId, adapter);
}
get(providerId: TeamRuntimeProviderId): TeamLaunchRuntimeAdapter {
const adapter = this.adapters.get(providerId);
if (!adapter) {
throw new Error(`Runtime adapter is not available for provider ${providerId}`);
}
return adapter;
}
has(providerId: TeamRuntimeProviderId): boolean {
return this.adapters.has(providerId);
}
providers(): TeamRuntimeProviderId[] {
return Array.from(this.adapters.keys());
}
}

View file

@ -0,0 +1,29 @@
export { OpenCodeTeamRuntimeAdapter } from './OpenCodeTeamRuntimeAdapter';
export type {
OpenCodeTeamLaunchMode,
OpenCodeTeamRuntimeAdapterOptions,
OpenCodeTeamRuntimeBridgePort,
} from './OpenCodeTeamRuntimeAdapter';
export {
isTeamRuntimeProviderId,
TeamRuntimeAdapterRegistry,
TEAM_RUNTIME_PROVIDER_IDS,
} from './TeamRuntimeAdapter';
export type {
TeamLaunchRuntimeAdapter,
TeamRuntimeLaunchInput,
TeamRuntimeLaunchResult,
TeamRuntimeMemberLaunchEvidence,
TeamRuntimeMemberSpec,
TeamRuntimeMemberStopEvidence,
TeamRuntimePrepareFailure,
TeamRuntimePrepareResult,
TeamRuntimePrepareSuccess,
TeamRuntimeProviderId,
TeamRuntimeReconcileInput,
TeamRuntimeReconcileReason,
TeamRuntimeReconcileResult,
TeamRuntimeStopInput,
TeamRuntimeStopReason,
TeamRuntimeStopResult,
} from './TeamRuntimeAdapter';

View file

@ -10,6 +10,7 @@ import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictP
import { BoardTaskExactLogSummarySelector } from '../exact/BoardTaskExactLogSummarySelector';
import { isBoardTaskExactLogsReadEnabled } from '../exact/featureGates';
import { getBoardTaskExactLogFileVersions } from '../exact/fileVersions';
import { OpenCodeTaskLogStreamSource } from './OpenCodeTaskLogStreamSource';
import type { BoardTaskExactLogDetailCandidate } from '../exact/BoardTaskExactLogTypes';
import type { ContentBlock, ParsedMessage, ToolUseResultData } from '@main/types';
@ -1078,7 +1079,8 @@ export class BoardTaskLogStreamService {
private readonly detailSelector: BoardTaskExactLogDetailSelector = new BoardTaskExactLogDetailSelector(),
private readonly chunkBuilder: BoardTaskExactLogChunkBuilder = new BoardTaskExactLogChunkBuilder(),
private readonly taskReader: TeamTaskReader = new TeamTaskReader(),
private readonly transcriptSourceLocator: TeamTranscriptSourceLocator = new TeamTranscriptSourceLocator()
private readonly transcriptSourceLocator: TeamTranscriptSourceLocator = new TeamTranscriptSourceLocator(),
private readonly runtimeFallbackSource: OpenCodeTaskLogStreamSource = new OpenCodeTaskLogStreamSource()
) {}
private async buildInferredExecutionSlices(
@ -1294,6 +1296,10 @@ export class BoardTaskLogStreamService {
teamName: string,
taskId: string
): Promise<BoardTaskLogStreamSummary> {
if (!isBoardTaskExactLogsReadEnabled()) {
return emptySummary();
}
const layout = await this.buildStreamLayout(teamName, taskId);
if (layout.visibleSlices.length === 0) {
return emptySummary();
@ -1305,9 +1311,15 @@ export class BoardTaskLogStreamService {
}
async getTaskLogStream(teamName: string, taskId: string): Promise<BoardTaskLogStreamResponse> {
if (!isBoardTaskExactLogsReadEnabled()) {
return emptyResponse();
}
const layout = await this.buildStreamLayout(teamName, taskId);
if (layout.visibleSlices.length === 0) {
return emptyResponse();
return (
(await this.runtimeFallbackSource.getTaskLogStream(teamName, taskId)) ?? emptyResponse()
);
}
const segments: BoardTaskLogSegment[] = [];
@ -1360,6 +1372,7 @@ export class BoardTaskLogStreamService {
participants: layout.participants,
defaultFilter,
segments,
source: 'transcript',
};
}
}

View file

@ -0,0 +1,293 @@
import {
OpenCodeTaskLogAttributionStore,
OpenCodeTaskLogAttributionRecord,
OpenCodeTaskLogAttributionScope,
OpenCodeTaskLogAttributionSource,
OpenCodeTaskLogAttributionWriteResult,
} from './OpenCodeTaskLogAttributionStore';
export interface OpenCodeTaskLogAttributionWriter {
upsertTaskRecord(
teamName: string,
record: OpenCodeTaskLogAttributionRecord,
options?: { now?: Date }
): Promise<OpenCodeTaskLogAttributionWriteResult>;
replaceTaskRecords(
teamName: string,
taskId: string,
records: OpenCodeTaskLogAttributionRecord[],
options?: { now?: Date }
): Promise<OpenCodeTaskLogAttributionWriteResult>;
clearTaskRecords(
teamName: string,
taskId: string
): Promise<OpenCodeTaskLogAttributionWriteResult>;
}
export interface OpenCodeTaskLogAttributionRecordDraft {
memberName: string;
scope?: OpenCodeTaskLogAttributionScope;
sessionId?: string;
since?: string | Date;
until?: string | Date;
startMessageUuid?: string;
endMessageUuid?: string;
source?: OpenCodeTaskLogAttributionSource;
}
export interface OpenCodeTaskLogAttributionTaskSessionInput {
teamName: string;
taskId: string;
memberName: string;
sessionId: string;
since?: string | Date;
until?: string | Date;
startMessageUuid?: string;
endMessageUuid?: string;
source?: OpenCodeTaskLogAttributionSource;
}
export interface OpenCodeTaskLogAttributionMemberWindowInput {
teamName: string;
taskId: string;
memberName: string;
sessionId?: string;
since?: string | Date;
until?: string | Date;
startMessageUuid?: string;
endMessageUuid?: string;
source?: OpenCodeTaskLogAttributionSource;
}
export interface OpenCodeTaskLogAttributionReplaceInput {
teamName: string;
taskId: string;
records: OpenCodeTaskLogAttributionRecordDraft[];
source?: OpenCodeTaskLogAttributionSource;
}
export interface OpenCodeTaskLogAttributionTaskInput {
teamName: string;
taskId: string;
}
export interface OpenCodeTaskLogAttributionRecordWriteOutcome {
result: OpenCodeTaskLogAttributionWriteResult;
record: OpenCodeTaskLogAttributionRecord;
}
export interface OpenCodeTaskLogAttributionBulkWriteOutcome {
result: OpenCodeTaskLogAttributionWriteResult;
recordCount: number;
}
const VALID_SOURCES = new Set<OpenCodeTaskLogAttributionSource>([
'manual',
'launch_runtime',
'reconcile',
]);
const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
const TASK_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$/;
const MEMBER_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
const MAX_RUNTIME_ID_LENGTH = 256;
function trimOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function requireString(field: string, value: unknown): string {
const trimmed = trimOptionalString(value);
if (!trimmed) {
throw new Error(`OpenCode task-log attribution ${field} is required`);
}
return trimmed;
}
function requirePatternString(field: string, value: unknown, pattern: RegExp): string {
const trimmed = requireString(field, value);
if (!pattern.test(trimmed)) {
throw new Error(`OpenCode task-log attribution ${field} contains invalid characters`);
}
return trimmed;
}
function trimRuntimeId(field: string, value: unknown): string | undefined {
const trimmed = trimOptionalString(value);
if (!trimmed) {
return undefined;
}
if (trimmed.length > MAX_RUNTIME_ID_LENGTH) {
throw new Error(
`OpenCode task-log attribution ${field} exceeds max length (${MAX_RUNTIME_ID_LENGTH})`
);
}
return trimmed;
}
function normalizeIso(field: string, value: string | Date | undefined): string | undefined {
if (value === undefined) {
return undefined;
}
const timestamp =
value instanceof Date ? value.getTime() : Date.parse(requireString(field, value));
if (!Number.isFinite(timestamp)) {
throw new Error(`OpenCode task-log attribution ${field} must be a valid timestamp`);
}
return new Date(timestamp).toISOString();
}
function normalizeScope(
value: OpenCodeTaskLogAttributionScope | undefined
): OpenCodeTaskLogAttributionScope {
if (value === undefined) {
return 'member_session_window';
}
if (value === 'task_session' || value === 'member_session_window') {
return value;
}
throw new Error('OpenCode task-log attribution scope is invalid');
}
function normalizeSource(
value: OpenCodeTaskLogAttributionSource | undefined,
fallback: OpenCodeTaskLogAttributionSource
): OpenCodeTaskLogAttributionSource {
const source = value ?? fallback;
if (!VALID_SOURCES.has(source)) {
throw new Error('OpenCode task-log attribution source is invalid');
}
return source;
}
function assertRecordPolicy(record: OpenCodeTaskLogAttributionRecord): void {
if (record.since && record.until && Date.parse(record.since) > Date.parse(record.until)) {
throw new Error('OpenCode task-log attribution since must be before or equal to until');
}
if (record.scope === 'task_session') {
if (!record.sessionId) {
throw new Error('OpenCode task-log attribution task_session requires sessionId');
}
return;
}
if (!record.since && !record.startMessageUuid) {
throw new Error(
'OpenCode task-log attribution member_session_window requires since or startMessageUuid'
);
}
}
function buildRecord(
taskId: string,
draft: OpenCodeTaskLogAttributionRecordDraft,
fallbackSource: OpenCodeTaskLogAttributionSource
): OpenCodeTaskLogAttributionRecord {
const sessionId = trimRuntimeId('sessionId', draft.sessionId);
const since = normalizeIso('since', draft.since);
const until = normalizeIso('until', draft.until);
const startMessageUuid = trimRuntimeId('startMessageUuid', draft.startMessageUuid);
const endMessageUuid = trimRuntimeId('endMessageUuid', draft.endMessageUuid);
const record: OpenCodeTaskLogAttributionRecord = {
taskId: requirePatternString('taskId', taskId, TASK_ID_PATTERN),
memberName: requirePatternString('memberName', draft.memberName, MEMBER_NAME_PATTERN),
scope: normalizeScope(draft.scope),
...(sessionId ? { sessionId } : {}),
...(since ? { since } : {}),
...(until ? { until } : {}),
...(startMessageUuid ? { startMessageUuid } : {}),
...(endMessageUuid ? { endMessageUuid } : {}),
source: normalizeSource(draft.source, fallbackSource),
};
assertRecordPolicy(record);
return record;
}
export class OpenCodeTaskLogAttributionService {
constructor(
private readonly writer: OpenCodeTaskLogAttributionWriter = new OpenCodeTaskLogAttributionStore(),
private readonly now: () => Date = () => new Date()
) {}
async recordTaskSession(
input: OpenCodeTaskLogAttributionTaskSessionInput
): Promise<OpenCodeTaskLogAttributionRecordWriteOutcome> {
const teamName = requirePatternString('teamName', input.teamName, TEAM_NAME_PATTERN);
const record = buildRecord(
requireString('taskId', input.taskId),
{
memberName: input.memberName,
scope: 'task_session',
sessionId: input.sessionId,
since: input.since,
until: input.until,
startMessageUuid: input.startMessageUuid,
endMessageUuid: input.endMessageUuid,
source: input.source,
},
'launch_runtime'
);
return {
result: await this.writer.upsertTaskRecord(teamName, record, { now: this.now() }),
record,
};
}
async recordMemberSessionWindow(
input: OpenCodeTaskLogAttributionMemberWindowInput
): Promise<OpenCodeTaskLogAttributionRecordWriteOutcome> {
const teamName = requirePatternString('teamName', input.teamName, TEAM_NAME_PATTERN);
const record = buildRecord(
requireString('taskId', input.taskId),
{
memberName: input.memberName,
scope: 'member_session_window',
sessionId: input.sessionId,
since: input.since,
until: input.until,
startMessageUuid: input.startMessageUuid,
endMessageUuid: input.endMessageUuid,
source: input.source,
},
'reconcile'
);
return {
result: await this.writer.upsertTaskRecord(teamName, record, { now: this.now() }),
record,
};
}
async replaceTaskAttribution(
input: OpenCodeTaskLogAttributionReplaceInput
): Promise<OpenCodeTaskLogAttributionBulkWriteOutcome> {
const teamName = requirePatternString('teamName', input.teamName, TEAM_NAME_PATTERN);
const taskId = requirePatternString('taskId', input.taskId, TASK_ID_PATTERN);
const fallbackSource = normalizeSource(input.source, 'reconcile');
const records = input.records.map((record) => buildRecord(taskId, record, fallbackSource));
return {
result: await this.writer.replaceTaskRecords(teamName, taskId, records, { now: this.now() }),
recordCount: records.length,
};
}
async clearTaskAttribution(
input: OpenCodeTaskLogAttributionTaskInput
): Promise<OpenCodeTaskLogAttributionBulkWriteOutcome> {
const teamName = requirePatternString('teamName', input.teamName, TEAM_NAME_PATTERN);
const taskId = requirePatternString('taskId', input.taskId, TASK_ID_PATTERN);
return {
result: await this.writer.clearTaskRecords(teamName, taskId),
recordCount: 0,
};
}
}

View file

@ -0,0 +1,481 @@
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { OPENCODE_TASK_LOG_ATTRIBUTION_FILE } from '@shared/constants/opencodeTaskLogAttribution';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as path from 'path';
import { atomicWriteAsync } from '../../atomicWrite';
import { withFileLock } from '../../fileLock';
const logger = createLogger('OpenCodeTaskLogAttributionStore');
const MAX_ATTRIBUTION_FILE_BYTES = 512 * 1024;
export type OpenCodeTaskLogAttributionScope = 'task_session' | 'member_session_window';
export type OpenCodeTaskLogAttributionSource = 'manual' | 'launch_runtime' | 'reconcile';
export interface OpenCodeTaskLogAttributionRecord {
taskId: string;
memberName: string;
scope: OpenCodeTaskLogAttributionScope;
sessionId?: string;
since?: string;
until?: string;
startMessageUuid?: string;
endMessageUuid?: string;
source?: OpenCodeTaskLogAttributionSource;
createdAt?: string;
updatedAt?: string;
}
interface RawAttributionRecord extends Record<string, unknown> {
taskId?: unknown;
}
interface OpenCodeTaskLogAttributionFile {
schemaVersion: 1;
tasks: Record<string, OpenCodeTaskLogAttributionRecord[]>;
}
export type OpenCodeTaskLogAttributionWriteResult = 'created' | 'updated' | 'unchanged' | 'deleted';
export interface OpenCodeTaskLogAttributionReader {
readTaskRecords(teamName: string, taskId: string): Promise<OpenCodeTaskLogAttributionRecord[]>;
}
function trimString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeIso(value: unknown): string | undefined {
const trimmed = trimString(value);
if (!trimmed) {
return undefined;
}
const parsed = Date.parse(trimmed);
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
}
function normalizeScope(value: unknown): OpenCodeTaskLogAttributionScope {
return value === 'task_session' ? 'task_session' : 'member_session_window';
}
function normalizeSource(value: unknown): OpenCodeTaskLogAttributionSource | undefined {
return value === 'manual' || value === 'launch_runtime' || value === 'reconcile'
? value
: undefined;
}
function normalizeRecord(
taskId: string,
raw: RawAttributionRecord
): OpenCodeTaskLogAttributionRecord | null {
const memberName = trimString(raw.memberName);
if (!memberName) {
return null;
}
const since = normalizeIso(raw.since);
const until = normalizeIso(raw.until);
if (since && until && Date.parse(since) > Date.parse(until)) {
return null;
}
const sessionId = trimString(raw.sessionId);
const startMessageUuid = trimString(raw.startMessageUuid);
const endMessageUuid = trimString(raw.endMessageUuid);
const source = normalizeSource(raw.source);
const createdAt = normalizeIso(raw.createdAt);
const updatedAt = normalizeIso(raw.updatedAt);
return {
taskId,
memberName,
scope: normalizeScope(raw.scope),
...(sessionId ? { sessionId } : {}),
...(since ? { since } : {}),
...(until ? { until } : {}),
...(startMessageUuid ? { startMessageUuid } : {}),
...(endMessageUuid ? { endMessageUuid } : {}),
...(source ? { source } : {}),
...(createdAt ? { createdAt } : {}),
...(updatedAt ? { updatedAt } : {}),
};
}
function extractRawRecords(parsed: unknown, taskId: string): RawAttributionRecord[] {
if (!parsed || typeof parsed !== 'object') {
return [];
}
const file = parsed as Record<string, unknown>;
if (file.schemaVersion !== 1) {
return [];
}
const rawRecords: RawAttributionRecord[] = [];
if (file.tasks && typeof file.tasks === 'object' && !Array.isArray(file.tasks)) {
const taskRecords = (file.tasks as Record<string, unknown>)[taskId];
if (Array.isArray(taskRecords)) {
for (const record of taskRecords) {
if (record && typeof record === 'object' && !Array.isArray(record)) {
rawRecords.push(record as RawAttributionRecord);
}
}
}
}
if (Array.isArray(file.records)) {
for (const record of file.records) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
continue;
}
const raw = record as RawAttributionRecord;
if (trimString(raw.taskId) === taskId) {
rawRecords.push(raw);
}
}
}
return rawRecords;
}
function extractAllRawRecords(parsed: unknown): RawAttributionRecord[] {
if (!parsed || typeof parsed !== 'object') {
return [];
}
const file = parsed as Record<string, unknown>;
if (file.schemaVersion !== 1) {
return [];
}
const rawRecords: RawAttributionRecord[] = [];
if (file.tasks && typeof file.tasks === 'object' && !Array.isArray(file.tasks)) {
for (const [taskId, taskRecords] of Object.entries(file.tasks as Record<string, unknown>)) {
if (!Array.isArray(taskRecords)) {
continue;
}
for (const record of taskRecords) {
if (record && typeof record === 'object' && !Array.isArray(record)) {
rawRecords.push({
...(record as RawAttributionRecord),
taskId,
});
}
}
}
}
if (Array.isArray(file.records)) {
for (const record of file.records) {
if (record && typeof record === 'object' && !Array.isArray(record)) {
rawRecords.push(record as RawAttributionRecord);
}
}
}
return rawRecords;
}
function dedupeRecords(
records: OpenCodeTaskLogAttributionRecord[]
): OpenCodeTaskLogAttributionRecord[] {
const deduped = new Map<string, OpenCodeTaskLogAttributionRecord>();
for (const record of records) {
deduped.set(
[
record.taskId,
record.memberName.trim().toLowerCase(),
record.scope,
record.sessionId ?? '',
record.since ?? '',
record.until ?? '',
record.startMessageUuid ?? '',
record.endMessageUuid ?? '',
].join('\0'),
record
);
}
return Array.from(deduped.values()).sort((left, right) => {
const leftStart = left.since ?? left.createdAt ?? '';
const rightStart = right.since ?? right.createdAt ?? '';
if (leftStart !== rightStart) {
return leftStart.localeCompare(rightStart);
}
return left.memberName.localeCompare(right.memberName);
});
}
function buildUpsertKey(record: OpenCodeTaskLogAttributionRecord): string {
return JSON.stringify([
record.taskId,
record.memberName.trim().toLowerCase(),
record.scope,
record.sessionId ?? '',
record.since ?? '',
record.startMessageUuid ?? '',
]);
}
function canonicalizeFile(
records: OpenCodeTaskLogAttributionRecord[]
): OpenCodeTaskLogAttributionFile {
const byTask = new Map<string, OpenCodeTaskLogAttributionRecord[]>();
for (const record of records) {
const existing = byTask.get(record.taskId) ?? [];
existing.push(record);
byTask.set(record.taskId, existing);
}
const tasks: Record<string, OpenCodeTaskLogAttributionRecord[]> = {};
for (const [taskId, taskRecords] of [...byTask.entries()].sort(([left], [right]) =>
left.localeCompare(right)
)) {
const normalized = dedupeRecords(taskRecords);
if (normalized.length > 0) {
tasks[taskId] = normalized;
}
}
return {
schemaVersion: 1,
tasks,
};
}
function normalizeRecordForWrite(
record: OpenCodeTaskLogAttributionRecord
): OpenCodeTaskLogAttributionRecord | null {
return normalizeRecord(record.taskId, record as unknown as RawAttributionRecord);
}
function sameJson(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function stripAuditFields(
record: OpenCodeTaskLogAttributionRecord
): Omit<OpenCodeTaskLogAttributionRecord, 'createdAt' | 'updatedAt'> {
const { createdAt: _createdAt, updatedAt: _updatedAt, ...rest } = record;
return rest;
}
export function getOpenCodeTaskLogAttributionPath(teamName: string): string {
return path.join(getTeamsBasePath(), teamName, OPENCODE_TASK_LOG_ATTRIBUTION_FILE);
}
export class OpenCodeTaskLogAttributionStore implements OpenCodeTaskLogAttributionReader {
private async readFileForWrite(filePath: string): Promise<OpenCodeTaskLogAttributionFile> {
try {
const stat = await fs.promises.stat(filePath);
if (!stat.isFile()) {
throw new Error(`OpenCode task-log attribution path is not a file: ${filePath}`);
}
if (stat.size > MAX_ATTRIBUTION_FILE_BYTES) {
throw new Error(`OpenCode task-log attribution file is too large: ${filePath}`);
}
const raw = await readFileUtf8WithTimeout(filePath, 5_000);
const parsed = JSON.parse(raw) as unknown;
if (
!parsed ||
typeof parsed !== 'object' ||
(parsed as { schemaVersion?: unknown }).schemaVersion !== 1
) {
throw new Error(`Unsupported OpenCode task-log attribution schema: ${filePath}`);
}
return canonicalizeFile(
extractAllRawRecords(parsed)
.map((record) => {
const taskId = trimString(record.taskId);
return taskId ? normalizeRecord(taskId, record) : null;
})
.filter((record): record is OpenCodeTaskLogAttributionRecord => record !== null)
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { schemaVersion: 1, tasks: {} };
}
if (error instanceof SyntaxError) {
throw new Error(`Invalid OpenCode task-log attribution JSON: ${filePath}`);
}
throw error;
}
}
private async writeFileIfChanged(
filePath: string,
previous: OpenCodeTaskLogAttributionFile,
next: OpenCodeTaskLogAttributionFile
): Promise<boolean> {
if (sameJson(previous, next)) {
return false;
}
await atomicWriteAsync(filePath, `${JSON.stringify(next, null, 2)}\n`);
return true;
}
async readTaskRecords(
teamName: string,
taskId: string
): Promise<OpenCodeTaskLogAttributionRecord[]> {
const filePath = getOpenCodeTaskLogAttributionPath(teamName);
try {
const stat = await fs.promises.stat(filePath);
if (!stat.isFile() || stat.size > MAX_ATTRIBUTION_FILE_BYTES) {
return [];
}
const raw = await readFileUtf8WithTimeout(filePath, 5_000);
const parsed = JSON.parse(raw) as unknown;
return dedupeRecords(
extractRawRecords(parsed, taskId)
.map((record) => normalizeRecord(taskId, record))
.filter((record): record is OpenCodeTaskLogAttributionRecord => record !== null)
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
if (error instanceof SyntaxError) {
logger.warn(`[${teamName}/${taskId}] invalid OpenCode task-log attribution JSON`);
return [];
}
if (error instanceof FileReadTimeoutError) {
logger.warn(`[${teamName}/${taskId}] OpenCode task-log attribution read timed out`);
return [];
}
logger.warn(
`[${teamName}/${taskId}] failed to read OpenCode task-log attribution: ${
error instanceof Error ? error.message : String(error)
}`
);
return [];
}
}
async upsertTaskRecord(
teamName: string,
record: OpenCodeTaskLogAttributionRecord,
options?: { now?: Date }
): Promise<OpenCodeTaskLogAttributionWriteResult> {
const normalized = normalizeRecordForWrite(record);
if (!normalized) {
throw new Error('Invalid OpenCode task-log attribution record');
}
const filePath = getOpenCodeTaskLogAttributionPath(teamName);
return withFileLock(filePath, async () => {
const previous = await this.readFileForWrite(filePath);
const now = (options?.now ?? new Date()).toISOString();
const taskRecords = previous.tasks[normalized.taskId] ?? [];
const targetKey = buildUpsertKey(normalized);
const existingIndex = taskRecords.findIndex(
(candidate) => buildUpsertKey(candidate) === targetKey
);
const existingRecord = existingIndex >= 0 ? taskRecords[existingIndex] : undefined;
if (
existingRecord &&
sameJson(stripAuditFields(existingRecord), stripAuditFields(normalized))
) {
return 'unchanged';
}
const nextRecord: OpenCodeTaskLogAttributionRecord = {
...normalized,
createdAt: existingRecord?.createdAt ?? normalized.createdAt ?? now,
updatedAt: now,
};
const nextTaskRecords =
existingIndex >= 0
? taskRecords.map((candidate, index) =>
index === existingIndex ? nextRecord : candidate
)
: [...taskRecords, nextRecord];
const next = canonicalizeFile([
...Object.entries(previous.tasks).flatMap(([taskId, records]) =>
taskId === normalized.taskId ? [] : records
),
...nextTaskRecords,
]);
const changed = await this.writeFileIfChanged(filePath, previous, next);
if (!changed) {
return 'unchanged';
}
return existingIndex >= 0 ? 'updated' : 'created';
});
}
async replaceTaskRecords(
teamName: string,
taskId: string,
records: OpenCodeTaskLogAttributionRecord[],
options?: { now?: Date }
): Promise<OpenCodeTaskLogAttributionWriteResult> {
const normalizedTaskId = trimString(taskId);
if (!normalizedTaskId) {
throw new Error('Invalid OpenCode task-log attribution task id');
}
const now = (options?.now ?? new Date()).toISOString();
const normalizedRecords = records.map((record) =>
normalizeRecordForWrite({
...record,
taskId: normalizedTaskId,
createdAt: record.createdAt ?? now,
updatedAt: record.updatedAt ?? now,
})
);
if (normalizedRecords.some((record) => record === null)) {
throw new Error('Invalid OpenCode task-log attribution record');
}
const validRecords = normalizedRecords as OpenCodeTaskLogAttributionRecord[];
const filePath = getOpenCodeTaskLogAttributionPath(teamName);
return withFileLock(filePath, async () => {
const previous = await this.readFileForWrite(filePath);
const next = canonicalizeFile([
...Object.entries(previous.tasks).flatMap(([candidateTaskId, taskRecords]) =>
candidateTaskId === normalizedTaskId ? [] : taskRecords
),
...validRecords,
]);
const changed = await this.writeFileIfChanged(filePath, previous, next);
return changed ? 'updated' : 'unchanged';
});
}
async clearTaskRecords(
teamName: string,
taskId: string
): Promise<OpenCodeTaskLogAttributionWriteResult> {
const normalizedTaskId = trimString(taskId);
if (!normalizedTaskId) {
throw new Error('Invalid OpenCode task-log attribution task id');
}
const filePath = getOpenCodeTaskLogAttributionPath(teamName);
return withFileLock(filePath, async () => {
const previous = await this.readFileForWrite(filePath);
if (!previous.tasks[normalizedTaskId]) {
return 'unchanged';
}
const next = canonicalizeFile(
Object.entries(previous.tasks).flatMap(([candidateTaskId, taskRecords]) =>
candidateTaskId === normalizedTaskId ? [] : taskRecords
)
);
await this.writeFileIfChanged(filePath, previous, next);
return 'deleted';
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -279,6 +279,8 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Codex';
case 'gemini':
return 'Gemini';
case 'opencode':
return 'OpenCode';
}
}
@ -1132,7 +1134,9 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
const handleProviderRefresh = useCallback(
(providerId: CliProviderId) => {
void fetchCliProviderStatus(providerId);
void fetchCliProviderStatus(providerId, {
verifyModels: providerId === 'opencode',
});
},
[fetchCliProviderStatus]
);
@ -1218,7 +1222,11 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
providerStatusLoading={cliProviderStatusLoading}
disabled={isBusy || cliStatusLoading || !renderCliStatus.binaryPath}
onSelectBackend={handleProviderBackendChange}
onRefreshProvider={(providerId) => fetchCliProviderStatus(providerId)}
onRefreshProvider={(providerId) =>
fetchCliProviderStatus(providerId, {
verifyModels: providerId === 'opencode',
})
}
onRequestLogin={(providerId) => setProviderTerminal({ providerId, action: 'login' })}
/>
{providerTerminal && renderCliStatus.binaryPath && (

View file

@ -61,7 +61,7 @@ const ProviderCapabilityCardSkeleton = ({
providerId,
displayName,
}: {
providerId: 'anthropic' | 'codex' | 'gemini';
providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode';
displayName: string;
}): React.JSX.Element => (
<div className="rounded-md border border-border bg-surface-raised px-3 py-2">

View file

@ -124,6 +124,8 @@ function getConnectionDescription(provider: CliProviderStatus): string {
return 'Choose whether Codex should prefer your ChatGPT subscription or an API key when the native runtime launches.';
case 'gemini':
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
case 'opencode':
return 'OpenCode authentication and provider inventory are managed by the OpenCode runtime.';
}
}
@ -135,6 +137,8 @@ function getRuntimeDescription(provider: CliProviderStatus): string {
return 'Codex now runs only through the native runtime path.';
case 'gemini':
return 'Choose which Gemini runtime backend multimodel should use.';
case 'opencode':
return 'OpenCode uses its own managed runtime host. Desktop currently exposes status only.';
}
}
@ -1093,6 +1097,26 @@ export const ProviderRuntimeSettingsDialog = ({
</span>
) : null}
</div>
{selectedProvider.detailMessage ? (
<div className="mt-2 text-xs" style={{ color: 'var(--color-text-secondary)' }}>
{selectedProvider.detailMessage}
</div>
) : null}
{selectedProvider.externalRuntimeDiagnostics &&
selectedProvider.externalRuntimeDiagnostics.length > 0 ? (
<div
className="mt-2 space-y-1 text-[11px]"
style={{ color: 'var(--color-text-muted)' }}
>
{selectedProvider.externalRuntimeDiagnostics.slice(0, 3).map((diagnostic) => (
<div key={diagnostic.id}>
{diagnostic.label}:{' '}
{diagnostic.statusMessage ?? (diagnostic.detected ? 'detected' : 'missing')}
{diagnostic.detailMessage ? ` - ${diagnostic.detailMessage}` : ''}
</div>
))}
</div>
) : null}
</div>
) : null}

View file

@ -122,6 +122,8 @@ function getProviderLabel(providerId: CliProviderId): string {
return 'Codex';
case 'gemini':
return 'Gemini';
case 'opencode':
return 'OpenCode';
}
}
@ -697,7 +699,11 @@ export const CliStatusSection = (): React.JSX.Element | null => {
providerStatusLoading={cliProviderStatusLoading}
disabled={!effectiveCliStatus.binaryPath || isBusy || cliStatusLoading}
onSelectBackend={handleRuntimeBackendChange}
onRefreshProvider={(providerId) => fetchCliProviderStatus(providerId)}
onRefreshProvider={(providerId) =>
fetchCliProviderStatus(providerId, {
verifyModels: providerId === 'opencode',
})
}
onRequestLogin={(providerId) =>
setProviderTerminal({ providerId, action: 'login' })
}

View file

@ -408,6 +408,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
anthropic: 0,
codex: 0,
gemini: 0,
opencode: 0,
};
for (const session of searchedSessions) {

View file

@ -14,6 +14,7 @@ export const SESSION_PROVIDER_IDS = [
'anthropic',
'codex',
'gemini',
'opencode',
] as const satisfies readonly TeamProviderId[];
interface SessionFiltersPopoverProps {

View file

@ -48,11 +48,16 @@ export function getProvisioningProviderBackendSummary(
const optionById = new Map(options.map((option) => [option.id, option.label]));
const effectiveBackendId = provider.resolvedBackendId ?? provider.selectedBackendId;
const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null;
const inferredProviderId =
provider.providerId ??
(effectiveBackendId === 'codex-native' || options.some((option) => option.id === 'codex-native')
? 'codex'
: undefined);
const inferredProviderId: TeamProviderId | undefined =
provider.providerId === 'anthropic' ||
provider.providerId === 'codex' ||
provider.providerId === 'gemini' ||
provider.providerId === 'opencode'
? provider.providerId
: effectiveBackendId === 'codex-native' ||
options.some((option) => option.id === 'codex-native')
? 'codex'
: undefined;
const normalizedLabel =
formatProviderBackendLabel(inferredProviderId, effectiveBackendId ?? undefined) ?? null;

View file

@ -28,22 +28,24 @@ import {
getProviderScopedTeamModelLabel,
getRuntimeAwareProviderScopedTeamModelLabel,
getTeamModelLabel as getCatalogTeamModelLabel,
getTeamModelSourceBadgeLabel,
getTeamProviderLabel as getCatalogTeamProviderLabel,
isAnthropicHaikuTeamModel,
} from '@renderer/utils/teamModelCatalog';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { resolveAnthropicLaunchModel } from '@shared/utils/anthropicLaunchModel';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
import { isTeamProviderId } from '@shared/utils/teamProvider';
import { AlertTriangle, Info } from 'lucide-react';
import type { CliProviderStatus } from '@shared/types';
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog';
// --- Provider definitions ---
interface ProviderDef {
id: 'anthropic' | 'codex' | 'gemini' | 'opencode';
id: TeamProviderId;
label: string;
comingSoon: boolean;
}
@ -55,13 +57,13 @@ const PROVIDERS: ProviderDef[] = [
{ id: 'opencode', label: 'OpenCode', comingSoon: false },
];
const OPENCODE_UI_DISABLED_REASON = 'OpenCode in development';
const OPENCODE_UI_DISABLED_REASON = 'OpenCode team launch is not ready.';
export function getTeamModelLabel(model: string): string {
return getCatalogTeamModelLabel(model) ?? model;
}
export function getTeamProviderLabel(providerId: 'anthropic' | 'codex' | 'gemini'): string {
export function getTeamProviderLabel(providerId: TeamProviderId): string {
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
}
@ -73,11 +75,15 @@ export function getTeamEffortLabel(effort: string): string {
}
export function formatTeamModelSummary(
providerId: 'anthropic' | 'codex' | 'gemini',
providerId: TeamProviderId,
model: string,
effort?: string
): string {
const providerLabel = getTeamProviderLabel(providerId);
const routeLabel =
providerId === 'opencode'
? (getTeamModelSourceBadgeLabel(providerId, model.trim()) ?? providerLabel)
: providerLabel;
const rawModelLabel = model.trim() ? getTeamModelLabel(model.trim()) : 'Default';
const modelLabel = model.trim()
? getProviderScopedTeamModelLabel(providerId, model.trim())
@ -93,7 +99,7 @@ export function formatTeamModelSummary(
const parts = modelAlreadyCarriesProviderBrand
? [modelLabel, effortLabel]
: providerActsAsBackendOnly
? [modelLabel, `via ${providerLabel}`, effortLabel]
? [modelLabel, `via ${routeLabel}`, effortLabel]
: [providerLabel, modelLabel, effortLabel];
return parts.filter(Boolean).join(' · ');
@ -108,7 +114,7 @@ export function formatTeamModelSummary(
export function computeEffectiveTeamModel(
selectedModel: string,
limitContext: boolean,
providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic',
providerId: TeamProviderId = 'anthropic',
providerStatus?: Pick<CliProviderStatus, 'providerId' | 'modelCatalog'> | null
): string | undefined {
if (providerId !== 'anthropic') {
@ -129,8 +135,8 @@ export function computeEffectiveTeamModel(
}
export interface TeamModelSelectorProps {
providerId: 'anthropic' | 'codex' | 'gemini';
onProviderChange: (providerId: 'anthropic' | 'codex' | 'gemini') => void;
providerId: TeamProviderId;
onProviderChange: (providerId: TeamProviderId) => void;
value: string;
onValueChange: (value: string) => void;
id?: string;
@ -158,6 +164,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
} = useEffectiveCliProviderStatus(effectiveProviderId);
const multimodelAvailable =
multimodelEnabled || effectiveCliStatus?.flavor === 'agent_teams_orchestrator';
const runtimeProviderStatusById = useMemo(
() =>
new Map(
(effectiveCliStatus?.providers ?? []).map((provider) => [provider.providerId, provider])
),
[effectiveCliStatus?.providers]
);
const defaultModelTooltip = useMemo(() => {
if (effectiveProviderId === 'anthropic') {
const defaultLongContextModel =
@ -179,7 +192,32 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
}, [effectiveProviderId, runtimeProviderStatus]);
const getProviderDisabledReason = (candidateProviderId: string): string | null => {
if (candidateProviderId === 'opencode') {
return OPENCODE_UI_DISABLED_REASON;
const providerStatus = runtimeProviderStatusById.get('opencode') ?? null;
if (!providerStatus) {
return 'OpenCode runtime status is still loading.';
}
if (!providerStatus.supported) {
return (
providerStatus.detailMessage ??
providerStatus.statusMessage ??
'OpenCode CLI is not installed.'
);
}
if (!providerStatus.authenticated) {
return (
providerStatus.detailMessage ??
providerStatus.statusMessage ??
'OpenCode has no connected provider.'
);
}
if (!providerStatus.capabilities.teamLaunch) {
return (
providerStatus.detailMessage ??
providerStatus.statusMessage ??
OPENCODE_UI_DISABLED_REASON
);
}
return null;
}
if (disableGeminiOption && isGeminiUiFrozen() && candidateProviderId === 'gemini') {
return GEMINI_UI_DISABLED_REASON;
@ -194,7 +232,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
const getProviderStatusBadge = (candidateProviderId: string): string | null => {
if (candidateProviderId === 'opencode') {
return 'In development';
return getProviderDisabledReason(candidateProviderId) ? 'Gated' : null;
}
const providerDisabledReason = getProviderDisabledReason(candidateProviderId);
@ -209,8 +247,8 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
return null;
};
const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => {
if (statusBadge === 'In development') {
return 'Dev';
if (statusBadge === 'Gated') {
return 'Gate';
}
if (statusBadge === 'Multimodel off') {
@ -250,10 +288,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
<Tabs
value={effectiveProviderId}
onValueChange={(nextValue) => {
if (
(nextValue === 'anthropic' || nextValue === 'codex' || nextValue === 'gemini') &&
isProviderSelectable(nextValue)
) {
if (isTeamProviderId(nextValue) && isProviderSelectable(nextValue)) {
onProviderChange(nextValue);
}
}}
@ -353,6 +388,10 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
availabilityStatus === 'available');
const modelStatusMessage =
modelIssueReason ?? modelDisabledReason ?? availabilityReason ?? null;
const sourceBadgeLabel =
effectiveProviderId === 'opencode' && opt.value !== ''
? opt.badgeLabel?.trim() || null
: null;
return (
<button
@ -382,6 +421,19 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
>
<span className="flex flex-col items-center justify-center gap-0.5">
<span className="leading-tight">{opt.label}</span>
{sourceBadgeLabel ? (
<span
className="rounded-full border px-2 py-0.5 text-[10px] font-medium"
style={{
borderColor: 'var(--color-border-subtle)',
backgroundColor: 'rgba(255, 255, 255, 0.04)',
color: 'var(--color-text-secondary)',
}}
title={`Source: ${sourceBadgeLabel}`}
>
{sourceBadgeLabel}
</span>
) : null}
{opt.value === '' && (
<span className="flex items-center justify-center gap-1">
<TooltipProvider delayDuration={200}>

View file

@ -2,7 +2,12 @@ import { isTeamEffortLevel } from '@shared/utils/effortLevels';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes';
import type { EffortLevel, ResolvedTeamMember, TeamProvisioningMemberInput } from '@shared/types';
import type {
EffortLevel,
ResolvedTeamMember,
TeamProviderId,
TeamProvisioningMemberInput,
} from '@shared/types';
function normalizeRestartSensitiveMemberContract(member: {
role?: string;
@ -13,7 +18,7 @@ function normalizeRestartSensitiveMemberContract(member: {
}): {
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: EffortLevel;
} {
@ -124,7 +129,7 @@ function normalizeEditableMemberSnapshot(member: {
name: string;
role?: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: EffortLevel;
} | null {

View file

@ -51,6 +51,8 @@ function normalizeResponse(response: BoardTaskLogStreamResponse): BoardTaskLogSt
return {
participants: response.participants,
defaultFilter: response.defaultFilter,
source: response.source,
runtimeProjection: response.runtimeProjection,
segments: response.segments.map((segment) => ({
...segment,
chunks: asEnhancedChunkArray(segment.chunks) ?? [],
@ -58,6 +60,22 @@ function normalizeResponse(response: BoardTaskLogStreamResponse): BoardTaskLogSt
};
}
function describeStreamSource(stream: BoardTaskLogStreamResponse | null): string {
if (stream?.source === 'opencode_runtime_attribution') {
return 'Task-scoped OpenCode runtime logs projected from explicit task attribution into the same execution-log components used in Logs.';
}
if (stream?.source === 'opencode_runtime_fallback') {
if (stream.runtimeProjection?.fallbackReason === 'task_tool_markers') {
const spanCount = stream.runtimeProjection.markerSpanCount;
const spanDetails =
typeof spanCount === 'number' && spanCount > 1 ? ` across ${spanCount} spans` : '';
return `Task-scoped OpenCode runtime logs projected from matched task tool markers${spanDetails} into the same execution-log components used in Logs.`;
}
return 'Task-scoped OpenCode runtime logs projected into the same execution-log components used in Logs.';
}
return 'Task-scoped transcript logs rendered with the same execution-log components used in Logs.';
}
const SegmentMarker = ({ segment }: { segment: BoardTaskLogSegment }): React.JSX.Element => {
return (
<div className="mb-2 flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
@ -211,11 +229,13 @@ export const TaskLogStreamSection = ({
};
const unsubscribe = api.teams.onTeamChange?.((_event, event) => {
if (
event.teamName !== teamName ||
event.type !== 'task-log-change' ||
event.taskId !== taskId
) {
if (event.teamName !== teamName) {
return;
}
const shouldReload =
event.type === 'log-source-change' ||
(event.type === 'task-log-change' && event.taskId === taskId);
if (!shouldReload) {
return;
}
scheduleReload();
@ -247,6 +267,7 @@ export const TaskLogStreamSection = ({
const participants = stream?.participants ?? [];
const showChips = participants.length > 1;
const streamDescription = useMemo(() => describeStreamSource(stream), [stream]);
const visibleSegments = useMemo(() => {
const source = stream?.segments ?? [];
const filtered =
@ -292,9 +313,7 @@ export const TaskLogStreamSection = ({
<h4 className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
Task Log Stream
</h4>
<p className="text-xs text-[var(--color-text-muted)]">
Task-scoped transcript logs rendered with the same execution-log components used in Logs.
</p>
<p className="text-xs text-[var(--color-text-muted)]">{streamDescription}</p>
{showChips ? (
<div className="flex flex-wrap items-center gap-1.5">
@ -331,8 +350,8 @@ export const TaskLogStreamSection = ({
<FileText size={20} className="mx-auto mb-2 opacity-40" />
No task log stream yet
<p className="mt-1 text-[10px] opacity-60">
Task-linked transcript logs will appear here when explicit task-linked transcript
metadata is available.
Task-linked logs will appear here when transcript metadata or runtime projection is
available.
</p>
</div>
) : (

View file

@ -10,6 +10,9 @@
*/
import { del, get, set } from 'idb-keyval';
import { isTeamProviderId } from '@shared/utils/teamProvider';
import type { TeamProviderId } from '@shared/types';
import { isTeamEffortLevel } from '@shared/utils/effortLevels';
@ -29,7 +32,7 @@ export interface SerializedMemberDraft {
roleSelection: string;
customRole: string;
workflow?: string;
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
model?: string;
effort?: EffortLevel;
}
@ -66,10 +69,7 @@ function isValidMember(m: unknown): m is SerializedMemberDraft {
typeof obj.name === 'string' &&
typeof obj.roleSelection === 'string' &&
typeof obj.customRole === 'string' &&
(obj.providerId === undefined ||
obj.providerId === 'anthropic' ||
obj.providerId === 'codex' ||
obj.providerId === 'gemini') &&
(obj.providerId === undefined || isTeamProviderId(obj.providerId)) &&
(obj.model === undefined || typeof obj.model === 'string') &&
(obj.effort === undefined || isTeamEffortLevel(obj.effort))
);

View file

@ -14,7 +14,12 @@ const logger = createLogger('Store:cliInstaller');
/** Max log lines to keep in UI (reserved for future use) */
const _MAX_LOG_LINES = 50;
export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'gemini'];
export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = [
'anthropic',
'codex',
'gemini',
'opencode',
];
export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
const providers: CliProviderStatus[] = (
@ -22,6 +27,7 @@ export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
{ providerId: 'anthropic', displayName: 'Anthropic' },
{ providerId: 'codex', displayName: 'Codex' },
{ providerId: 'gemini', displayName: 'Gemini' },
{ providerId: 'opencode', displayName: 'OpenCode' },
] as const
).map((provider) => ({
...provider,
@ -33,7 +39,7 @@ export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
statusMessage: 'Checking...',
models: [],
modelAvailability: [],
canLoginFromUi: true,
canLoginFromUi: provider.providerId !== 'opencode',
capabilities: {
teamLaunch: false,
oneShot: false,

View file

@ -1522,7 +1522,7 @@ export interface GlobalTaskDetailState {
/** Per-team launch parameters shown in the header badge. */
export interface TeamLaunchParams {
providerId?: 'anthropic' | 'codex' | 'gemini';
providerId?: TeamProviderId;
providerBackendId?: string;
model?: string; // 'opus' | 'sonnet' | 'haiku'
effort?: EffortLevel;

View file

@ -5,7 +5,7 @@ import {
getTeamProviderLabel,
} from '@renderer/utils/teamModelCatalog';
import type { InboxMessage } from '@shared/types';
import type { InboxMessage, TeamProviderId } from '@shared/types';
const BOOTSTRAP_REQUIRED_MARKER_SETS = [
[
@ -31,11 +31,14 @@ const BOOTSTRAP_SUPPORTING_MARKERS = [
'resume your queue normally and prioritize already-assigned board work',
] as const;
type TeamProviderId = 'anthropic' | 'codex' | 'gemini';
function parseProviderId(value: string | undefined): TeamProviderId | null {
const normalized = value?.trim().toLowerCase();
if (normalized === 'anthropic' || normalized === 'codex' || normalized === 'gemini') {
if (
normalized === 'anthropic' ||
normalized === 'codex' ||
normalized === 'gemini' ||
normalized === 'opencode'
) {
return normalized;
}
return null;

View file

@ -283,6 +283,8 @@ function getRuntimeAdvisoryProviderLabel(providerId: TeamProviderId | undefined)
return 'Codex';
case 'gemini':
return 'Gemini';
case 'opencode':
return 'OpenCode';
default:
return null;
}

View file

@ -4,6 +4,7 @@ import {
isTeamProviderBackendId,
migrateProviderBackendId,
} from '@shared/utils/providerBackend';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import type { CliProviderStatus, TeamProviderBackendId, TeamProviderId } from '@shared/types';
@ -27,8 +28,9 @@ export function resolveUiOwnedProviderBackendId(
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined,
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
): TeamProviderBackendId | undefined {
const normalizedProviderId = normalizeOptionalTeamProviderId(providerId);
return migrateProviderBackendId(
providerId,
normalizedProviderId,
provider?.selectedBackendId ?? provider?.resolvedBackendId
);
}

View file

@ -3,6 +3,7 @@ import {
getRuntimeAwareProviderScopedTeamModelLabel,
getRuntimeAwareTeamModelBadgeLabel,
getRuntimeAwareTeamModelUiDisabledReason,
getTeamModelSourceBadgeLabel,
getTeamProviderLabel,
getTeamProviderModelOptions,
getVisibleTeamProviderModels,
@ -314,6 +315,10 @@ export function getAvailableTeamProviderModelOptions(
return {
value: model,
label: getProviderScopedTeamModelLabel(providerId, model) ?? model,
badgeLabel:
providerId === 'opencode'
? (getTeamModelSourceBadgeLabel(providerId, model) ?? undefined)
: undefined,
availabilityStatus: getRuntimeModelAvailability(providerId, model, providerStatus),
availabilityReason: getRuntimeModelAvailabilityReason(model, providerStatus),
};

View file

@ -1,5 +1,14 @@
import { parseModelString } from '@shared/utils/modelParser';
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
import {
getOpenCodeQualifiedModelSourceLabel,
parseOpenCodeQualifiedModelRef,
} from '@shared/utils/opencodeModelRef';
import {
filterVisibleProviderRuntimeModels,
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
} from '@shared/utils/providerModelVisibility';
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
@ -38,6 +47,7 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
anthropic: 'Anthropic',
codex: 'Codex',
gemini: 'Gemini',
opencode: 'OpenCode',
};
const ANTHROPIC_ALIAS_LABELS = {
@ -133,12 +143,16 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
badgeLabel: '2.5-flash-lite',
},
],
opencode: [{ value: '', label: 'Default', badgeLabel: 'Default' }],
};
const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>> = {
anthropic: new Map(ANTHROPIC_MODEL_ORDER.map((model, index) => [model, index])),
codex: new Map(TEAM_PROVIDER_MODEL_OPTIONS.codex.map((option, index) => [option.value, index])),
gemini: new Map(TEAM_PROVIDER_MODEL_OPTIONS.gemini.map((option, index) => [option.value, index])),
opencode: new Map(
TEAM_PROVIDER_MODEL_OPTIONS.opencode.map((option, index) => [option.value, index])
),
};
function getKnownTeamProviderModelOption(
@ -247,12 +261,15 @@ export function getTeamModelLabel(model: string | undefined): string | undefined
return undefined;
}
const overrideLabel = TEAM_MODEL_LABEL_OVERRIDES[trimmed];
const parsedOpenCodeModel = parseOpenCodeQualifiedModelRef(trimmed);
const labelTarget = parsedOpenCodeModel?.modelId ?? trimmed;
const overrideLabel = TEAM_MODEL_LABEL_OVERRIDES[labelTarget];
if (overrideLabel) {
return overrideLabel;
}
return formatParsedClaudeModelLabel(trimmed) ?? trimmed;
return formatParsedClaudeModelLabel(labelTarget) ?? labelTarget;
}
function getRuntimeCatalogModel(
@ -299,9 +316,23 @@ export function getTeamModelBadgeLabel(
if (providerId === 'gemini') {
return trimmed.replace(/^gemini-/, '');
}
if (providerId === 'opencode') {
return getTeamModelLabel(trimmed) ?? trimmed;
}
return trimmed;
}
export function getTeamModelSourceBadgeLabel(
providerId: SupportedProviderId,
model: string | undefined
): string | undefined {
if (providerId !== 'opencode') {
return undefined;
}
return getOpenCodeQualifiedModelSourceLabel(model) ?? undefined;
}
export function getProviderScopedTeamModelLabel(
providerId: SupportedProviderId,
model: string | undefined

View file

@ -1,6 +1,8 @@
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
import type { TeamProviderId } from '@shared/types';
import type { CliProviderId, TeamProviderId } from '@shared/types';
type SupportedProviderId = CliProviderId | TeamProviderId;
export function stripTrailingOneMillionSuffixes(model: string | undefined): string | undefined {
const trimmed = model?.trim();
@ -13,7 +15,7 @@ export function stripTrailingOneMillionSuffixes(model: string | undefined): stri
export function extractProviderScopedBaseModel(
model: string | undefined,
providerId?: TeamProviderId
providerId?: SupportedProviderId
): string | undefined {
const trimmed = model?.trim();
if (!trimmed) {

View file

@ -0,0 +1 @@
export const OPENCODE_TASK_LOG_ATTRIBUTION_FILE = 'opencode-task-log-attribution.json';

View file

@ -33,7 +33,7 @@ export type CliPlatform =
export type CliFlavor = 'claude' | 'agent_teams_orchestrator';
export type CliProviderId = 'anthropic' | 'codex' | 'gemini';
export type CliProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type CliProviderAuthMode = 'auto' | 'oauth' | 'chatgpt' | 'api_key';
export interface CliProviderConnectionInfo {
@ -194,6 +194,7 @@ export interface CliProviderStatus {
verificationState: 'verified' | 'unknown' | 'offline' | 'error';
modelVerificationState?: 'idle' | 'verifying' | 'verified';
statusMessage?: string | null;
detailMessage?: string | null;
models: string[];
modelCatalog?: CliProviderModelCatalog | null;
modelAvailability?: CliProviderModelAvailability[];

View file

@ -336,10 +336,25 @@ export interface BoardTaskLogSegment {
chunks: EnhancedChunk[];
}
export interface BoardTaskLogStreamRuntimeProjection {
provider: 'opencode';
mode: 'attribution' | 'heuristic';
attributionRecordCount: number;
projectedMessageCount: number;
fallbackReason?:
| 'no_attribution_records'
| 'attribution_no_projected_messages'
| 'task_tool_markers';
markerMatchCount?: number;
markerSpanCount?: number;
}
export interface BoardTaskLogStreamResponse {
participants: BoardTaskLogParticipant[];
defaultFilter: 'all' | string;
segments: BoardTaskLogSegment[];
source?: 'transcript' | 'opencode_runtime_fallback' | 'opencode_runtime_attribution';
runtimeProjection?: BoardTaskLogStreamRuntimeProjection;
}
export interface BoardTaskLogStreamSummary {
@ -450,11 +465,7 @@ export interface TeamTask {
}
/** Task enriched for UI/DTO use (overlay from kanban-state.json). */
export type TaskChangePresenceState =
| 'has_changes'
| 'needs_attention'
| 'no_changes'
| 'unknown';
export type TaskChangePresenceState = 'has_changes' | 'needs_attention' | 'no_changes' | 'unknown';
export interface TeamTaskWithKanban extends TeamTask {
/** Set when task is in team kanban (review or approved column). */
@ -787,7 +798,7 @@ export interface TeamViewSnapshot {
}
export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini' | 'opencode';
export type TeamProviderBackendId = 'auto' | 'adapter' | 'api' | 'cli-sdk' | 'codex-native';
export type TeamFastMode = 'inherit' | 'on' | 'off';

View file

@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest';
import { inferTeamProviderIdFromModel } from '../teamProvider';
import {
inferTeamProviderIdFromModel,
isTeamProviderId,
normalizeOptionalTeamProviderId,
} from '../teamProvider';
describe('inferTeamProviderIdFromModel', () => {
it('recognizes Anthropic aliases with 1m suffixes', () => {
@ -13,5 +17,13 @@ describe('inferTeamProviderIdFromModel', () => {
expect(inferTeamProviderIdFromModel('claude-opus-4-6')).toBe('anthropic');
expect(inferTeamProviderIdFromModel('gpt-5.4')).toBe('codex');
expect(inferTeamProviderIdFromModel('gemini-2.5-pro')).toBe('gemini');
expect(inferTeamProviderIdFromModel('opencode/default')).toBe('opencode');
expect(inferTeamProviderIdFromModel('openai/gpt-5.4')).toBe('opencode');
expect(inferTeamProviderIdFromModel('openrouter/moonshotai/kimi-k2')).toBe('opencode');
});
it('treats OpenCode as a valid explicit team provider id', () => {
expect(isTeamProviderId('opencode')).toBe(true);
expect(normalizeOptionalTeamProviderId('opencode')).toBe('opencode');
});
});

View file

@ -0,0 +1,78 @@
export interface OpenCodeQualifiedModelRef {
sourceId: string;
modelId: string;
raw: string;
}
const OPEN_CODE_MODEL_REF_PATTERN = /^(?<source>[a-z0-9-]+)\/(?<model>\S.*)$/i;
const OPEN_CODE_SOURCE_LABELS: Record<string, string> = {
anthropic: 'Anthropic',
azure: 'Azure',
bedrock: 'Bedrock',
deepseek: 'DeepSeek',
gemini: 'Gemini',
google: 'Google',
groq: 'Groq',
minimax: 'MiniMax',
mistral: 'Mistral',
moonshot: 'Moonshot',
ollama: 'Ollama',
opencode: 'OpenCode',
openai: 'OpenAI',
'openai-compatible': 'OpenAI Compatible',
openrouter: 'OpenRouter',
together: 'Together',
vertex: 'Vertex',
xai: 'xAI',
'z-ai': 'Z.AI',
};
function humanizeOpenCodeSourceId(sourceId: string): string {
const normalized = sourceId.trim().toLowerCase();
if (!normalized) {
return sourceId;
}
const knownLabel = OPEN_CODE_SOURCE_LABELS[normalized];
if (knownLabel) {
return knownLabel;
}
return normalized
.split('-')
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(' ');
}
export function parseOpenCodeQualifiedModelRef(
model: string | undefined | null
): OpenCodeQualifiedModelRef | null {
const trimmed = model?.trim();
if (!trimmed) {
return null;
}
const match = OPEN_CODE_MODEL_REF_PATTERN.exec(trimmed);
if (!match?.groups?.source || !match.groups.model) {
return null;
}
return {
raw: trimmed,
sourceId: match.groups.source.toLowerCase(),
modelId: match.groups.model,
};
}
export function getOpenCodeQualifiedModelSourceLabel(
model: string | undefined | null
): string | null {
const parsed = parseOpenCodeQualifiedModelRef(model);
if (!parsed) {
return null;
}
return humanizeOpenCodeSourceId(parsed.sourceId);
}

View file

@ -1,7 +1,9 @@
import type { TeamProviderId } from '@shared/types';
import { parseOpenCodeQualifiedModelRef } from './opencodeModelRef';
export function isTeamProviderId(value: unknown): value is TeamProviderId {
return value === 'anthropic' || value === 'codex' || value === 'gemini';
return value === 'anthropic' || value === 'codex' || value === 'gemini' || value === 'opencode';
}
export function normalizeOptionalTeamProviderId(value: unknown): TeamProviderId | undefined {
@ -24,6 +26,13 @@ export function inferTeamProviderIdFromModel(
}
const normalizedWithoutExtendedContextSuffix = normalized.replace(/(?:\[1m\])+$/, '');
if (
normalized.startsWith('opencode/') ||
normalizedWithoutExtendedContextSuffix.startsWith('opencode/')
) {
return 'opencode';
}
if (
normalized.startsWith('gpt-') ||
normalized.startsWith('codex') ||
@ -53,5 +62,12 @@ export function inferTeamProviderIdFromModel(
return 'anthropic';
}
if (
parseOpenCodeQualifiedModelRef(normalized) ||
parseOpenCodeQualifiedModelRef(normalizedWithoutExtendedContextSuffix)
) {
return 'opencode';
}
return undefined;
}

View file

@ -91,6 +91,8 @@ function formatProviderName(providerId: string): string {
return 'Codex';
case 'gemini':
return 'Gemini';
case 'opencode':
return 'OpenCode';
default:
return providerId;
}

View file

@ -89,6 +89,10 @@ declare module 'agent-teams-controller' {
launchTeam(flags: Record<string, unknown>): Promise<unknown>;
stopTeam(flags?: Record<string, unknown>): Promise<unknown>;
getRuntimeState(flags?: Record<string, unknown>): Promise<unknown>;
runtimeBootstrapCheckin(flags: Record<string, unknown>): Promise<unknown>;
runtimeDeliverMessage(flags: Record<string, unknown>): Promise<unknown>;
runtimeTaskEvent(flags: Record<string, unknown>): Promise<unknown>;
runtimeHeartbeat(flags: Record<string, unknown>): Promise<unknown>;
}
export interface AgentBlocksApi {

View file

@ -407,6 +407,188 @@ describe('CliInstallerService', () => {
)
).toBe(false);
});
it('uses execution-grade OpenCode model verification for explicit verify requests', async () => {
allowConsoleLogs();
vi.mocked(getConfiguredCliFlavor).mockReturnValue('agent_teams_orchestrator');
vi.mocked(getCliFlavorUiOptions).mockReturnValue({
displayName: 'agent_teams_orchestrator',
supportsSelfUpdate: false,
showVersionDetails: false,
showBinaryPath: false,
});
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/usr/local/bin/claude');
vi.spyOn(ClaudeMultimodelBridgeService.prototype, 'getProviderStatuses').mockResolvedValue([
{
providerId: 'anthropic',
displayName: 'Anthropic',
supported: true,
authenticated: true,
authMethod: 'oauth_token',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: { teamLaunch: true, oneShot: true, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
},
{
providerId: 'codex',
displayName: 'Codex',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: { teamLaunch: false, oneShot: false, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
},
{
providerId: 'gemini',
displayName: 'Gemini',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: { teamLaunch: false, oneShot: false, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
},
{
providerId: 'opencode',
displayName: 'OpenCode',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: ['openai/gpt-5.4-mini', 'opencode/big-pickle'],
modelAvailability: [],
canLoginFromUi: false,
capabilities: { teamLaunch: false, oneShot: false, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
connection: null,
},
] as never);
vi.spyOn(ClaudeMultimodelBridgeService.prototype, 'verifyProviderStatus').mockResolvedValue({
providerId: 'opencode',
displayName: 'OpenCode',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: ['openai/gpt-5.4-mini', 'opencode/big-pickle'],
modelAvailability: [],
canLoginFromUi: false,
capabilities: { teamLaunch: false, oneShot: false, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
connection: null,
} as never);
const verifyOpenCodeModelsSpy = vi
.spyOn(ClaudeMultimodelBridgeService.prototype, 'verifyOpenCodeModels')
.mockResolvedValue({
providerId: 'opencode',
displayName: 'OpenCode',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
modelVerificationState: 'verified',
statusMessage: null,
detailMessage: null,
models: ['openai/gpt-5.4-mini', 'opencode/big-pickle'],
modelAvailability: [
{
modelId: 'openai/gpt-5.4-mini',
status: 'unavailable',
reason: 'Token refresh failed: 401',
},
{
modelId: 'opencode/big-pickle',
status: 'available',
reason: null,
},
],
canLoginFromUi: false,
capabilities: { teamLaunch: false, oneShot: false, extensions: undefined as never },
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
connection: null,
} as never);
vi.mocked(execCli).mockImplementation(async (_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (normalizedArgs === '--version') {
return { stdout: '2.3.4', stderr: '' };
}
throw new Error(`Unexpected execCli call: ${normalizedArgs}`);
});
const status = await service.getStatus();
expect(status.providers.find((provider) => provider.providerId === 'opencode')?.modelAvailability).toEqual([]);
const verifiedProvider = await service.verifyProviderModels('opencode');
expect(verifyOpenCodeModelsSpy).toHaveBeenCalledTimes(1);
expect(verifiedProvider?.modelVerificationState).toBe('verified');
expect(verifiedProvider?.modelAvailability).toEqual([
expect.objectContaining({
modelId: 'openai/gpt-5.4-mini',
status: 'unavailable',
}),
expect.objectContaining({
modelId: 'opencode/big-pickle',
status: 'available',
}),
]);
});
});
describe('install mutex', () => {

View file

@ -62,6 +62,7 @@ import * as fsp from 'fs/promises';
import { errorDetector } from '../../../../src/main/services/error/ErrorDetector';
import { DataCache } from '../../../../src/main/services/infrastructure/DataCache';
import { FileWatcher } from '../../../../src/main/services/infrastructure/FileWatcher';
import { OPENCODE_TASK_LOG_ATTRIBUTION_FILE } from '../../../../src/shared/constants/opencodeTaskLogAttribution';
function createFakeWatcher(): FsType.FSWatcher {
const emitter = new EventEmitter() as EventEmitter & { close: () => void };
@ -168,6 +169,27 @@ describe('FileWatcher', () => {
watcher.stop();
});
it('emits log-source-change when OpenCode task-log attribution manifest changes', () => {
const dataCache = new DataCache(50, 10, false);
const watcher = new FileWatcher(dataCache, '/tmp/projects', '/tmp/todos');
const events: unknown[] = [];
watcher.on('team-change', (event) => events.push(event));
(
watcher as unknown as {
processTeamsChange: (eventType: string, filename: string) => void;
}
).processTeamsChange('change', `team-a/${OPENCODE_TASK_LOG_ATTRIBUTION_FILE}`);
expect(events).toEqual([
{
type: 'log-source-change',
teamName: 'team-a',
detail: OPENCODE_TASK_LOG_ATTRIBUTION_FILE,
},
]);
});
it('keeps append offset pinned for partial trailing lines until completed', async () => {
vi.useRealTimers();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'filewatcher-'));

View file

@ -182,7 +182,7 @@ describe('ClaudeMultimodelBridgeService', () => {
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator');
expect(providers).toHaveLength(3);
expect(providers).toHaveLength(4);
expect(providers[0]).toMatchObject({
providerId: 'anthropic',
authenticated: true,
@ -218,6 +218,18 @@ describe('ClaudeMultimodelBridgeService', () => {
projectId: 'demo-project',
},
});
expect(providers[3]).toMatchObject({
providerId: 'opencode',
displayName: 'OpenCode',
supported: false,
authenticated: false,
models: [],
canLoginFromUi: false,
capabilities: {
teamLaunch: false,
oneShot: false,
},
});
});
it('overrides provider auth status when provider-aware env reports a missing API key', async () => {
@ -693,4 +705,313 @@ describe('ClaudeMultimodelBridgeService', () => {
statusMessage: 'Codex CLI not found',
});
});
it('uses live OpenCode verification on explicit provider verify', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (normalizedArgs === 'runtime status --json --provider opencode') {
return Promise.resolve({
stdout: JSON.stringify({
providers: {
opencode: {
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
canLoginFromUi: false,
statusMessage: null,
detailMessage: 'version 1.4.0 - connected openai',
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: {
plugins: { status: 'read-only', ownership: 'provider-scoped', reason: null },
mcp: { status: 'read-only', ownership: 'provider-scoped', reason: null },
skills: { status: 'read-only', ownership: 'provider-scoped', reason: null },
apiKeys: { status: 'read-only', ownership: 'provider-scoped', reason: null },
},
},
models: ['openai/gpt-5.4-mini'],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
externalRuntimeDiagnostics: [],
},
},
}),
stderr: '',
exitCode: 0,
});
}
if (normalizedArgs === 'runtime verify --json --provider opencode') {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
snapshot: {
detected: true,
hostHealthy: true,
probeError: null,
diagnostics: [],
host: {
version: '1.4.0',
resolvedConfigFingerprint: 'resolved-fingerprint-123456',
},
profile: {
profileRootKey: 'profile-root',
projectBehaviorFingerprint: 'behavior-fingerprint-123456',
managedConfigFingerprint: 'managed-fingerprint-123456',
},
config: {
default_agent: 'teammate',
share: 'disabled',
snapshot: false,
autoupdate: false,
},
},
}),
stderr: '',
exitCode: 0,
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const provider = await service.verifyProviderStatus('/mock/agent_teams_orchestrator', 'opencode');
expect(provider).toMatchObject({
providerId: 'opencode',
verificationState: 'verified',
detailMessage: expect.stringContaining('live resolved-fin'),
backend: {
kind: 'opencode-cli',
authMethodDetail: 'managed teammate agent',
},
});
expect(provider.externalRuntimeDiagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'opencode-live-host',
detected: true,
statusMessage: 'Healthy',
}),
expect.objectContaining({
id: 'opencode-managed-runtime',
detected: true,
statusMessage: 'Managed runtime verified',
}),
])
);
});
it('loads projected OpenCode transcript data through the runtime transcript command', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (
normalizedArgs
=== 'runtime transcript --json --provider opencode --team team-a --member alice --limit 20'
) {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
transcript: {
sessionId: 'session-1',
durableState: 'idle',
messageCount: 2,
toolCallCount: 1,
errorCount: 0,
latestAssistantText: '/tmp/project',
latestAssistantPreview: '/tmp/project',
messages: [],
diagnostics: [],
logProjection: {
sessionId: 'session-1',
durableState: 'idle',
sourceMessageCount: 2,
projectedMessageCount: 3,
syntheticMessageCount: 1,
toolCallCount: 1,
errorCount: 0,
diagnostics: [],
messages: [
{
uuid: 'msg-assistant-1',
type: 'assistant',
toolCalls: [{ id: 'call_pwd', name: 'bash' }],
},
{
uuid: 'msg-assistant-1::tool_results',
type: 'user',
isMeta: true,
toolResults: [{ toolUseId: 'call_pwd', isError: false }],
},
],
},
},
}),
stderr: '',
exitCode: 0,
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const transcript = await service.getOpenCodeTranscript('/mock/agent_teams_orchestrator', {
teamId: 'team-a',
memberName: 'alice',
limit: 20,
});
expect(transcript).toMatchObject({
sessionId: 'session-1',
durableState: 'idle',
toolCallCount: 1,
logProjection: {
projectedMessageCount: 3,
syntheticMessageCount: 1,
messages: expect.arrayContaining([
expect.objectContaining({
uuid: 'msg-assistant-1',
type: 'assistant',
}),
expect.objectContaining({
uuid: 'msg-assistant-1::tool_results',
type: 'user',
isMeta: true,
}),
]),
},
});
});
it('verifies OpenCode models through execution-grade runtime probes', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (
normalizedArgs
=== 'runtime verify-model --json --provider opencode --model openai/gpt-5.4-mini'
) {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
result: {
modelId: 'openai/gpt-5.4-mini',
outcome: 'unavailable',
reason: 'Token refresh failed: 401',
},
}),
stderr: '',
exitCode: 0,
});
}
if (
normalizedArgs
=== 'runtime verify-model --json --provider opencode --model opencode/big-pickle'
) {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
result: {
modelId: 'opencode/big-pickle',
outcome: 'available',
reason: null,
},
}),
stderr: '',
exitCode: 0,
});
}
if (
normalizedArgs
=== 'runtime verify-model --json --provider opencode --model openrouter/moonshotai/kimi-k2'
) {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 1,
providerId: 'opencode',
result: {
modelId: 'openrouter/moonshotai/kimi-k2',
outcome: 'available',
reason: null,
},
}),
stderr: '',
exitCode: 0,
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const provider = await service.verifyOpenCodeModels('/mock/agent_teams_orchestrator', {
providerId: 'opencode',
displayName: 'OpenCode',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
modelVerificationState: 'idle',
statusMessage: null,
detailMessage: null,
models: ['openai/gpt-5.4-mini', 'openrouter/moonshotai/kimi-k2', 'opencode/big-pickle'],
modelAvailability: [],
canLoginFromUi: false,
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: {
plugins: { status: 'read-only', ownership: 'provider-scoped', reason: null },
mcp: { status: 'read-only', ownership: 'provider-scoped', reason: null },
skills: { status: 'read-only', ownership: 'provider-scoped', reason: null },
apiKeys: { status: 'read-only', ownership: 'provider-scoped', reason: null },
},
},
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
connection: null,
});
expect(provider.modelVerificationState).toBe('verified');
expect(provider.modelAvailability).toEqual([
expect.objectContaining({
modelId: 'openai/gpt-5.4-mini',
status: 'unavailable',
reason: 'Token refresh failed: 401',
}),
expect.objectContaining({
modelId: 'openrouter/moonshotai/kimi-k2',
status: 'available',
reason: null,
}),
expect.objectContaining({
modelId: 'opencode/big-pickle',
status: 'available',
reason: null,
}),
]);
});
});

View file

@ -38,6 +38,7 @@ describe('providerRuntimeEnv', () => {
it('preserves gemini as a valid team provider id', () => {
expect(resolveTeamProviderId('gemini')).toBe('gemini');
expect(resolveTeamProviderId('codex')).toBe('codex');
expect(resolveTeamProviderId('opencode')).toBe('opencode');
expect(resolveTeamProviderId(undefined)).toBe('anthropic');
});
});

View file

@ -108,6 +108,61 @@ describe('BoardTaskLogStreamService', () => {
expect(recordSource.getTaskRecords).not.toHaveBeenCalled();
});
it('falls back to OpenCode runtime stream when transcript slices are empty', async () => {
const runtimeFallbackSource = {
getTaskLogStream: vi.fn(async () => ({
participants: [
{
key: 'member:alice',
label: 'alice',
role: 'member' as const,
isLead: false,
isSidechain: true,
},
],
defaultFilter: 'member:alice',
segments: [
{
id: 'opencode:segment-1',
participantKey: 'member:alice',
actor: {
memberName: 'alice',
role: 'member' as const,
sessionId: 'session-opencode',
isSidechain: true,
},
startTimestamp: '2026-04-21T10:00:00.000Z',
endTimestamp: '2026-04-21T10:01:00.000Z',
chunks: [{ id: 'chunk-1' }],
},
],
source: 'opencode_runtime_fallback' as const,
})),
};
const service = new BoardTaskLogStreamService(
{
getTaskRecords: vi.fn(async () => []),
} as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
undefined as never,
runtimeFallbackSource as never
);
const response = await service.getTaskLogStream('demo', 'task-a');
expect(response.source).toBe('opencode_runtime_fallback');
expect(response.segments).toHaveLength(1);
expect(await service.getTaskLogStreamSummary('demo', 'task-a')).toEqual({
segmentCount: 0,
});
expect(runtimeFallbackSource.getTaskLogStream).toHaveBeenCalledTimes(1);
});
it('groups contiguous slices into participant segments and excludes lead slices when member slices exist', async () => {
const tom = {
memberName: 'tom',

View file

@ -0,0 +1,223 @@
import { describe, expect, it } from 'vitest';
import {
createEmptyEndpointMap,
createOpenCodeApiDiscoverySnapshot,
detectOpenCodeApiCapabilities,
resolveMissingOpenCodeCapabilities,
type OpenCodeApiEndpointMap,
} from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
describe('OpenCodeApiCapabilities', () => {
it('proves production launch capabilities from OpenAPI v1.14-style paths', async () => {
const fetch = fakeFetch({
'/doc': jsonResponse(openApiDocument()),
'/global/health': jsonResponse({ version: '1.14.19' }),
});
await expect(
detectOpenCodeApiCapabilities({
baseUrl: 'http://127.0.0.1:4096',
fetchImpl: fetch,
timeoutMs: 100,
})
).resolves.toMatchObject({
version: '1.14.19',
source: 'openapi_doc',
endpoints: {
permissionReply: true,
experimentalToolIds: true,
},
evidence: {
permissionReply: 'openapi',
experimentalToolIds: 'openapi',
},
requiredForTeamLaunch: {
ready: true,
missing: [],
},
});
});
it('keeps launch blocked when no permission reply route is proven', async () => {
const document = openApiDocument({
withoutPaths: ['/permission/{requestID}/reply'],
});
const fetch = fakeFetch({
'/doc': jsonResponse(document),
'/global/health': jsonResponse({ version: '1.14.19' }),
});
const capabilities = await detectOpenCodeApiCapabilities({
baseUrl: 'http://127.0.0.1:4096',
fetchImpl: fetch,
timeoutMs: 100,
});
expect(capabilities.endpoints.permissionReply).toBe(false);
expect(capabilities.requiredForTeamLaunch).toEqual({
ready: false,
missing: ['POST permission reply route'],
});
expect(capabilities.diagnostics).toContain(
'OpenCode permission response endpoint was not proven by OpenAPI; require real permission E2E before production launch'
);
});
it('accepts the legacy session permission response route as compatibility fallback', async () => {
const document = openApiDocument({
withoutPaths: ['/permission/{requestID}/reply'],
extraPaths: {
'/session/{sessionID}/permissions/{permissionID}': { post: {} },
},
});
const fetch = fakeFetch({
'/doc': jsonResponse(document),
'/global/health': jsonResponse({ version: '1.14.19' }),
});
const capabilities = await detectOpenCodeApiCapabilities({
baseUrl: 'http://127.0.0.1:4096',
fetchImpl: fetch,
timeoutMs: 100,
});
expect(capabilities.endpoints.permissionReply).toBe(false);
expect(capabilities.endpoints.permissionLegacySessionRespond).toBe(true);
expect(capabilities.requiredForTeamLaunch).toEqual({ ready: true, missing: [] });
});
it('uses safe direct probes as evidence when OpenAPI doc is unavailable', async () => {
const fetch = fakeFetch({
'/doc': new Response('missing', { status: 404 }),
'/doc.json': new Response('<html>not json</html>', { status: 200 }),
'/openapi.json': new Response('missing', { status: 404 }),
'/global/health': jsonResponse({ build: { version: '1.14.19' } }),
'/session/status': jsonResponse({}),
'/permission/': jsonResponse([]),
'/event': eventStreamResponse(),
'/global/event': eventStreamResponse(),
'/mcp': jsonResponse([]),
'/experimental/tool/ids': jsonResponse(['agent-teams_runtime_deliver_message']),
});
const capabilities = await detectOpenCodeApiCapabilities({
baseUrl: 'http://127.0.0.1:4096',
fetchImpl: fetch,
timeoutMs: 100,
});
expect(capabilities.version).toBe('1.14.19');
expect(capabilities.source).toBe('direct_probe');
expect(capabilities.endpoints.permissionList).toBe(true);
expect(capabilities.evidence.permissionList).toBe('direct_probe');
expect(capabilities.requiredForTeamLaunch.ready).toBe(false);
expect(capabilities.requiredForTeamLaunch.missing).toContain('POST /session');
expect(capabilities.requiredForTeamLaunch.missing).toContain('POST permission reply route');
});
it('uses experimental tool list as fallback for tool availability proof', () => {
const endpoints: OpenCodeApiEndpointMap = {
...createEmptyEndpointMap(),
health: true,
sessionCreate: true,
sessionGet: true,
sessionMessageList: true,
sessionPromptAsync: true,
sessionAbort: true,
sessionStatus: true,
permissionList: true,
permissionReply: true,
sessionEventStream: true,
globalEventStream: true,
mcpList: true,
mcpCreate: true,
experimentalToolIds: false,
experimentalToolList: true,
};
expect(resolveMissingOpenCodeCapabilities(endpoints)).toEqual([]);
});
it('redacts credentials and hashes the OpenAPI document in discovery snapshots', async () => {
const capabilities = await detectOpenCodeApiCapabilities({
baseUrl: 'http://user:secret@127.0.0.1:4096',
fetchImpl: fakeFetch({
'/doc': jsonResponse(openApiDocument()),
'/global/health': jsonResponse({ version: '1.14.19' }),
}),
timeoutMs: 100,
});
const snapshot = createOpenCodeApiDiscoverySnapshot({
baseUrl: 'http://user:secret@127.0.0.1:4096',
checkedAt: '2026-04-21T12:00:00.000Z',
capabilities,
openApiDocument: openApiDocument(),
});
expect(snapshot).toMatchObject({
checkedAt: '2026-04-21T12:00:00.000Z',
opencodeVersion: '1.14.19',
baseUrlRedacted: 'http://redacted:redacted@127.0.0.1:4096/',
});
expect(snapshot.openApiHash).toMatch(/^[a-f0-9]{64}$/);
});
});
function openApiDocument(options: {
withoutPaths?: string[];
extraPaths?: Record<string, Record<string, unknown>>;
} = {}): Record<string, unknown> {
const paths: Record<string, Record<string, unknown>> = {
'/global/health': { get: {} },
'/session': { post: {} },
'/session/{id}': { get: {} },
'/session/{id}/message': { get: {} },
'/session/{id}/prompt_async': { post: {} },
'/session/{id}/abort': { post: {} },
'/session/status': { get: {} },
'/permission': { get: {} },
'/permission/{requestID}/reply': { post: {} },
'/event': { get: {} },
'/global/event': { get: {} },
'/mcp': { get: {}, post: {} },
'/experimental/tool/ids': { get: {} },
...options.extraPaths,
};
for (const path of options.withoutPaths ?? []) {
delete paths[path];
}
return {
openapi: '3.1.0',
info: { title: 'OpenCode', version: '1.14.19' },
paths,
};
}
function fakeFetch(routes: Record<string, Response>): typeof fetch {
return (async (input: RequestInfo | URL) => {
const url = new URL(String(input));
const response = routes[url.pathname];
if (!response) {
return new Response('not found', { status: 404 });
}
return response.clone();
}) as typeof fetch;
}
function jsonResponse(value: unknown): Response {
return new Response(JSON.stringify(value), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
function eventStreamResponse(): Response {
return new Response('', {
status: 200,
headers: { 'content-type': 'text/event-stream' },
});
}

View file

@ -0,0 +1,269 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
OpenCodeBridgeCommandClient,
redactBridgeDiagnosticText,
type OpenCodeBridgeDiagnosticsSink,
type OpenCodeBridgeProcessRunInput,
type OpenCodeBridgeProcessRunResult,
type OpenCodeBridgeProcessRunner,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
import type {
OpenCodeBridgeDiagnosticEvent,
OpenCodeBridgeSuccess,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
let tempDir: string;
let runner: FakeBridgeProcessRunner;
let diagnostics: FakeDiagnosticsSink;
describe('OpenCodeBridgeCommandClient', () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-bridge-client-'));
runner = new FakeBridgeProcessRunner();
diagnostics = new FakeDiagnosticsSink();
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('writes a private input envelope, executes the bridge command, and removes the input file', async () => {
runner.nextResult = {
stdout: `${JSON.stringify(bridgeSuccess({ data: { runId: 'run-1' } }))}\n`,
stderr: '',
exitCode: 0,
timedOut: false,
};
const client = createClient();
const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, {
cwd: '/tmp/project',
timeoutMs: 10_000,
});
expect(result).toMatchObject({
ok: true,
requestId: 'req-1',
command: 'opencode.launchTeam',
});
expect(runner.calls).toHaveLength(1);
expect(runner.calls[0]).toMatchObject({
binaryPath: '/usr/local/bin/agent-teams-controller',
args: ['runtime', 'opencode-command', '--json', '--input', expect.any(String)],
cwd: '/tmp/project',
timeoutMs: 10_000,
});
const inputPath = runner.calls[0].args[4];
expect(JSON.parse(await runner.readInputEnvelope(0))).toMatchObject({
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.launchTeam',
cwd: '/tmp/project',
timeoutMs: 10_000,
body: { runId: 'run-1' },
});
await expect(fs.access(inputPath)).rejects.toThrow();
});
it('fails closed when stdout contains logs plus json', async () => {
runner.nextResult = {
stdout: 'debug token=secret\n{"ok":true}\n',
stderr: '',
exitCode: 0,
timedOut: false,
};
const client = createClient();
const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, {
cwd: '/tmp/project',
timeoutMs: 10_000,
});
expect(result).toMatchObject({
ok: false,
error: {
kind: 'contract_violation',
retryable: false,
},
});
expect(diagnostics.append).toHaveBeenCalledWith(
expect.objectContaining({
type: 'opencode_bridge_contract_violation',
severity: 'error',
runId: 'run-1',
data: {
stdoutPreview: 'debug token=[redacted]\n{"ok":true}\n',
},
})
);
});
it('records bridge timeout as unknown outcome with redacted diagnostics', async () => {
runner.nextResult = {
stdout: '',
stderr: 'Authorization: Bearer live-token',
exitCode: null,
timedOut: true,
};
const client = createClient();
const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, {
cwd: '/tmp/project',
timeoutMs: 10_000,
});
expect(result).toMatchObject({
ok: false,
error: {
kind: 'timeout',
retryable: true,
details: {
stderr: 'Authorization: Bearer [redacted]',
},
},
diagnostics: [
expect.objectContaining({
type: 'opencode_bridge_unknown_outcome',
severity: 'warning',
}),
],
});
});
it('turns non-zero process exit into provider_error without parsing stdout', async () => {
runner.nextResult = {
stdout: `${JSON.stringify(bridgeSuccess())}\n`,
stderr: 'api_key=secret failed',
exitCode: 2,
timedOut: false,
};
const client = createClient();
const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, {
cwd: '/tmp/project',
timeoutMs: 10_000,
});
expect(result).toMatchObject({
ok: false,
error: {
kind: 'provider_error',
retryable: true,
details: {
exitCode: 2,
stderr: 'api_key=[redacted] failed',
},
},
});
});
it('rejects bridge result envelope mismatches before caller can mutate state', async () => {
runner.nextResult = {
stdout: `${JSON.stringify(bridgeSuccess({ requestId: 'other-req' }))}\n`,
stderr: '',
exitCode: 0,
timedOut: false,
};
const client = createClient();
const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, {
cwd: '/tmp/project',
timeoutMs: 10_000,
});
expect(result).toMatchObject({
ok: false,
error: {
kind: 'contract_violation',
message: 'OpenCode bridge requestId mismatch',
retryable: false,
},
});
});
});
describe('redactBridgeDiagnosticText', () => {
it('redacts common secret forms and caps large payloads', () => {
const value = `token=abc password:secret Authorization: Bearer live ${'x'.repeat(5000)}`;
const redacted = redactBridgeDiagnosticText(value);
expect(redacted).toContain('token=[redacted]');
expect(redacted).toContain('password:[redacted]');
expect(redacted).toContain('Authorization: Bearer [redacted]');
expect(redacted).toContain('[truncated]');
expect(redacted.length).toBeLessThan(4_100);
});
});
function createClient(): OpenCodeBridgeCommandClient {
return new OpenCodeBridgeCommandClient({
binaryPath: '/usr/local/bin/agent-teams-controller',
tempDirectory: tempDir,
processRunner: runner,
diagnostics,
requestIdFactory: () => 'req-1',
diagnosticIdFactory: () => 'diag-1',
clock: () => new Date('2026-04-21T12:00:00.000Z'),
env: { PATH: '/usr/bin' },
});
}
function bridgeSuccess(
overrides: Partial<OpenCodeBridgeSuccess<unknown>> = {}
): OpenCodeBridgeSuccess<unknown> {
return {
ok: true,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.launchTeam',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.0.0',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data: {
runId: 'run-1',
},
...overrides,
};
}
class FakeBridgeProcessRunner implements OpenCodeBridgeProcessRunner {
calls: OpenCodeBridgeProcessRunInput[] = [];
inputEnvelopes: string[] = [];
nextResult: OpenCodeBridgeProcessRunResult = {
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false,
};
async run(input: OpenCodeBridgeProcessRunInput): Promise<OpenCodeBridgeProcessRunResult> {
this.calls.push(input);
this.inputEnvelopes.push(await fs.readFile(input.args[4], 'utf8'));
return this.nextResult;
}
async readInputEnvelope(index: number): Promise<string> {
return this.inputEnvelopes[index];
}
}
class FakeDiagnosticsSink implements OpenCodeBridgeDiagnosticsSink {
readonly events: OpenCodeBridgeDiagnosticEvent[] = [];
readonly append = vi.fn(async (event: OpenCodeBridgeDiagnosticEvent) => {
this.events.push(event);
});
}

View file

@ -0,0 +1,303 @@
import { describe, expect, it } from 'vitest';
import {
assertBridgeEvidenceCanCommitToRuntimeStores,
assertBridgeResultCanMutateState,
createOpenCodeBridgeHandshakeIdentityHash,
createOpenCodeBridgeIdempotencyKey,
parseSingleBridgeJsonResult,
stableHash,
validateBridgeResultEnvelope,
validateOpenCodeBridgeHandshake,
type OpenCodeBridgeCommandEnvelope,
type OpenCodeBridgeHandshake,
type OpenCodeBridgePeerIdentity,
type OpenCodeBridgeRuntimeSnapshot,
type OpenCodeBridgeSuccess,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
describe('OpenCodeBridgeCommandContract', () => {
it('rejects bridge stdout with logs plus json', () => {
const result = parseSingleBridgeJsonResult('debug log\n{"ok":true}\n');
expect(result).toEqual({
ok: false,
error: 'Bridge stdout must contain exactly one JSON line, got 2',
});
});
it('parses exactly one bridge result JSON line', () => {
const stdout = `${JSON.stringify(
bridgeSuccess({
data: {
runId: 'run-1',
idempotencyKey: 'key-1',
runtimeStoreManifestHighWatermark: 10,
},
})
)}\n`;
const parsed = parseSingleBridgeJsonResult(stdout);
expect(parsed).toMatchObject({
ok: true,
value: {
ok: true,
requestId: 'req-1',
command: 'opencode.launchTeam',
},
});
});
it('validates result request id and command against the command envelope', () => {
const envelope: OpenCodeBridgeCommandEnvelope<Record<string, never>> = {
schemaVersion: 1,
requestId: 'req-expected',
command: 'opencode.launchTeam',
cwd: '/tmp/project',
startedAt: '2026-04-21T12:00:00.000Z',
timeoutMs: 10_000,
body: {},
};
expect(validateBridgeResultEnvelope(bridgeSuccess({ requestId: 'other' }), envelope)).toEqual({
ok: false,
reason: 'OpenCode bridge requestId mismatch',
});
expect(
validateBridgeResultEnvelope(
bridgeSuccess({ requestId: 'req-expected', command: 'opencode.stopTeam' }),
envelope
)
).toEqual({
ok: false,
reason: 'OpenCode bridge command mismatch',
});
});
it('does not allow state mutation when capability snapshot mismatches', () => {
const result = bridgeSuccess({
runtime: { capabilitySnapshotId: 'old-snapshot' },
data: { runId: 'run-1' },
});
expect(() =>
assertBridgeResultCanMutateState(result, {
requestId: 'req-1',
command: 'opencode.launchTeam',
runId: 'run-1',
capabilitySnapshotId: 'new-snapshot',
})
).toThrow('OpenCode bridge capability snapshot mismatch');
});
it('allows state mutation when caller has no capability snapshot evidence to compare', () => {
const result = bridgeSuccess({
runtime: { capabilitySnapshotId: 'runtime-snapshot' },
data: { runId: 'run-1' },
});
expect(() =>
assertBridgeResultCanMutateState(result, {
requestId: 'req-1',
command: 'opencode.launchTeam',
runId: 'run-1',
capabilitySnapshotId: null,
})
).not.toThrow();
});
it('rejects state-changing bridge evidence with stale manifest or idempotency mismatch', () => {
const result = bridgeSuccess({
data: {
runId: 'run-1',
idempotencyKey: 'key-1',
runtimeStoreManifestHighWatermark: 9,
},
});
expect(() =>
assertBridgeEvidenceCanCommitToRuntimeStores({
result,
requestId: 'req-1',
command: 'opencode.launchTeam',
runId: 'run-1',
capabilitySnapshotId: 'cap-1',
manifest: { highWatermark: 10 },
idempotencyKey: 'key-1',
})
).toThrow('Bridge result manifest high watermark is stale');
expect(() =>
assertBridgeEvidenceCanCommitToRuntimeStores({
result: bridgeSuccess({
data: {
runId: 'run-1',
idempotencyKey: 'other-key',
runtimeStoreManifestHighWatermark: 10,
},
}),
requestId: 'req-1',
command: 'opencode.launchTeam',
runId: 'run-1',
capabilitySnapshotId: 'cap-1',
manifest: { highWatermark: 10 },
idempotencyKey: 'key-1',
})
).toThrow('Bridge result idempotency key mismatch');
});
it('rejects handshake when server manifest high watermark is stale', () => {
const client = peerIdentity('claude_team');
const server = peerIdentity('agent_teams_orchestrator', {
runtimeStoreManifestHighWatermark: 9,
});
const handshake = buildHandshake({ client, server });
expect(
validateOpenCodeBridgeHandshake({
handshake,
expectedClient: client,
requiredCommand: 'opencode.launchTeam',
expectedCapabilitySnapshotId: 'cap-1',
expectedManifestHighWatermark: 10,
expectedRunId: 'run-1',
})
).toEqual({
ok: false,
reason: 'Bridge server runtime manifest high watermark is stale',
});
});
it('rejects handshake when identity hash does not match peer evidence', () => {
const client = peerIdentity('claude_team');
const server = peerIdentity('agent_teams_orchestrator');
const handshake: OpenCodeBridgeHandshake = {
...buildHandshake({ client, server }),
identityHash: 'tampered',
};
expect(
validateOpenCodeBridgeHandshake({
handshake,
expectedClient: client,
requiredCommand: 'opencode.launchTeam',
expectedCapabilitySnapshotId: 'cap-1',
expectedManifestHighWatermark: 10,
expectedRunId: 'run-1',
})
).toEqual({
ok: false,
reason: 'Bridge handshake identity hash mismatch',
});
});
it('creates deterministic idempotency keys for equivalent JSON bodies', () => {
const first = createOpenCodeBridgeIdempotencyKey({
command: 'opencode.launchTeam',
teamName: 'Team A',
runId: 'run-1',
body: { a: 1, b: { c: true, d: ['x'] } },
});
const second = createOpenCodeBridgeIdempotencyKey({
command: 'opencode.launchTeam',
teamName: 'Team A',
runId: 'run-1',
body: { b: { d: ['x'], c: true }, a: 1 },
});
expect(first).toBe(second);
expect(first).toMatch(/^opencode:opencode.launchTeam:Team_A:run-1:[a-f0-9]{32}$/);
expect(stableHash({ b: 2, a: 1 })).toBe(stableHash({ a: 1, b: 2 }));
});
});
type BridgeSuccessOverrides = Omit<Partial<OpenCodeBridgeSuccess<unknown>>, 'runtime'> & {
runtime?: Partial<OpenCodeBridgeRuntimeSnapshot>;
data?: unknown;
};
function bridgeSuccess(overrides: BridgeSuccessOverrides = {}): OpenCodeBridgeSuccess<unknown> {
const { runtime: runtimeOverrides, ...rest } = overrides;
return {
ok: true,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.launchTeam',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.0.0',
capabilitySnapshotId: 'cap-1',
...runtimeOverrides,
},
diagnostics: [],
data: {
runId: 'run-1',
idempotencyKey: 'key-1',
runtimeStoreManifestHighWatermark: 10,
},
...rest,
};
}
function peerIdentity(
peer: OpenCodeBridgePeerIdentity['peer'],
runtimeOverrides: Partial<OpenCodeBridgePeerIdentity['runtime']> = {}
): OpenCodeBridgePeerIdentity {
return {
schemaVersion: 1,
peer,
appVersion: '1.0.0',
gitSha: 'git-1',
buildId: 'build-1',
bridgeProtocol: {
minVersion: 1,
currentVersion: 1,
supportedCommands: [
'opencode.handshake',
'opencode.commandStatus',
'opencode.launchTeam',
'opencode.stopTeam',
],
},
runtime: {
providerId: 'opencode',
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.0.0',
capabilitySnapshotId: 'cap-1',
runtimeStoreManifestHighWatermark: 10,
activeRunId: 'run-1',
...runtimeOverrides,
},
featureFlags: {
opencodeTeamLaunch: true,
opencodeStateChangingCommands: true,
},
};
}
function buildHandshake(input: {
client: OpenCodeBridgePeerIdentity;
server: OpenCodeBridgePeerIdentity;
}): OpenCodeBridgeHandshake {
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
schemaVersion: 1,
requestId: 'handshake-1',
client: input.client,
server: input.server,
agreedProtocolVersion: 1,
acceptedCommands: ['opencode.launchTeam', 'opencode.stopTeam'],
serverTime: '2026-04-21T12:00:00.000Z',
};
return {
...withoutHash,
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
};
}

View file

@ -0,0 +1,234 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { stableHash } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import {
createOpenCodeBridgeCommandLeaseStore,
createOpenCodeBridgeCommandLedgerStore,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore';
describe('OpenCodeBridgeCommandLedgerStore', () => {
let tempDir: string;
let now: Date;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-bridge-ledger-'));
now = new Date('2026-04-21T12:00:00.000Z');
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('blocks idempotency key reuse with a different payload', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
await expect(
ledger.begin({
idempotencyKey: 'same',
requestId: 'req-1',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash: stableHash({ prompt: 'first' }),
})
).resolves.toBe('started');
await expect(
ledger.begin({
idempotencyKey: 'same',
requestId: 'req-2',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash: stableHash({ prompt: 'second' }),
})
).rejects.toThrow('OpenCode bridge idempotency key reused with different payload');
});
it('marks timeout as unknown outcome and blocks retry until recovery', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
const requestHash = stableHash({ teamName: 'team-a', runId: 'run-1' });
await ledger.begin({
idempotencyKey: 'launch:team-a:run-1',
requestId: 'req-1',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
});
await ledger.markUnknownAfterTimeout({
idempotencyKey: 'launch:team-a:run-1',
error: 'timeout',
});
await expect(
ledger.begin({
idempotencyKey: 'launch:team-a:run-1',
requestId: 'req-2',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).rejects.toThrow('OpenCode bridge command outcome must be reconciled before retry');
await expect(ledger.getByIdempotencyKey('launch:team-a:run-1')).resolves.toMatchObject({
status: 'unknown_after_timeout',
retryable: false,
lastError: 'timeout',
});
});
it('allows same-payload duplicate only after a completed command', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
const requestHash = stableHash({ body: 'same' });
await ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-1',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
});
await expect(
ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-2',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).rejects.toThrow('OpenCode bridge command already started');
await ledger.markCompleted({
idempotencyKey: 'key-1',
response: { ok: true, runId: 'run-1' },
});
await expect(
ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-3',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).resolves.toBe('duplicate_same_payload_completed');
});
});
describe('OpenCodeBridgeCommandLeaseStore', () => {
let tempDir: string;
let now: Date;
let nextId: number;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-bridge-lease-'));
now = new Date('2026-04-21T12:00:00.000Z');
nextId = 1;
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('serializes state-changing commands per team through an active lease', async () => {
const leaseStore = createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(tempDir, 'leases.json'),
idFactory: () => `lease-${nextId++}`,
clock: () => now,
});
const first = await leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.launchTeam',
ttlMs: 10_000,
});
expect(first).toMatchObject({
leaseId: 'lease-1',
state: 'active',
expiresAt: '2026-04-21T12:00:10.000Z',
});
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.stopTeam',
ttlMs: 10_000,
})
).rejects.toThrow('OpenCode bridge command lease already active: lease-1');
await leaseStore.release('lease-1');
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.stopTeam',
ttlMs: 10_000,
})
).resolves.toMatchObject({
leaseId: 'lease-2',
command: 'opencode.stopTeam',
state: 'active',
});
});
it('expires stale active leases before acquiring a new one', async () => {
const leaseStore = createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(tempDir, 'leases.json'),
idFactory: () => `lease-${nextId++}`,
clock: () => now,
});
await leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.launchTeam',
ttlMs: 1000,
});
now = new Date('2026-04-21T12:00:02.000Z');
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.reconcileTeam',
ttlMs: 1000,
})
).resolves.toMatchObject({
leaseId: 'lease-2',
state: 'active',
});
const persisted = JSON.parse(
await fs.readFile(path.join(tempDir, 'leases.json'), 'utf8')
) as {
data: Array<{ leaseId: string; state: string }>;
};
expect(persisted.data).toEqual([
expect.objectContaining({ leaseId: 'lease-1', state: 'expired' }),
expect.objectContaining({ leaseId: 'lease-2', state: 'active' }),
]);
});
});

View file

@ -0,0 +1,240 @@
import { describe, expect, it } from 'vitest';
import {
mapOpenCodeStatusToDurableState,
normalizeOpenCodeEvent,
normalizeOpenCodeSessionStatus,
} from '../../../../src/main/services/team/opencode/events/OpenCodeEventNormalizer';
describe('OpenCodeEventNormalizer', () => {
it('normalizes v1.14 session.status object', () => {
expect(
normalizeOpenCodeEvent({
type: 'session.status',
properties: {
sessionID: 'ses_1',
status: { type: 'retry', attempt: 2, message: 'rate limited', next: 123 },
},
})
).toMatchObject({
kind: 'session_status',
sessionId: 'ses_1',
status: {
type: 'retry',
retryAttempt: 2,
retryMessage: 'rate limited',
retryNextAt: 123,
rawShape: 'v1.14',
},
});
});
it('normalizes legacy string status and active compatibility status', () => {
expect(normalizeOpenCodeSessionStatus('active')).toMatchObject({
type: 'busy',
rawShape: 'legacy-string',
});
expect(normalizeOpenCodeSessionStatus('idle')).toMatchObject({
type: 'idle',
rawShape: 'legacy-string',
});
expect(normalizeOpenCodeSessionStatus('unexpected')).toMatchObject({
type: 'unknown',
rawShape: 'legacy-string',
});
});
it('normalizes deprecated session.idle as an idle session status', () => {
expect(
normalizeOpenCodeEvent({
type: 'session.idle',
properties: { sessionID: 'ses_1' },
})
).toMatchObject({
kind: 'session_status',
sessionId: 'ses_1',
status: {
type: 'idle',
rawShape: 'v1.14',
},
});
});
it('normalizes global event envelopes without losing directory evidence', () => {
expect(
normalizeOpenCodeEvent({
directory: '/repo',
payload: {
type: 'server.heartbeat',
properties: {},
},
})
).toEqual({
kind: 'server_heartbeat',
scope: 'global',
directory: '/repo',
raw: {
directory: '/repo',
payload: {
type: 'server.heartbeat',
properties: {},
},
},
});
});
it('normalizes message.updated role and message id from info snapshot', () => {
expect(
normalizeOpenCodeEvent({
type: 'message.updated',
properties: {
sessionID: 'ses_1',
info: { id: 'msg_1', role: 'assistant' },
},
})
).toMatchObject({
kind: 'message_updated',
sessionId: 'ses_1',
messageId: 'msg_1',
role: 'assistant',
});
});
it('normalizes message.part.updated snapshots separately from streaming deltas', () => {
expect(
normalizeOpenCodeEvent({
type: 'message.part.updated',
properties: {
sessionID: 'ses_1',
part: {
id: 'part_1',
messageID: 'msg_1',
type: 'text',
text: 'complete text',
},
},
})
).toMatchObject({
kind: 'message_part_updated',
sessionId: 'ses_1',
messageId: 'msg_1',
partId: 'part_1',
partType: 'text',
textSnapshot: 'complete text',
});
});
it('normalizes streaming text from message.part.delta', () => {
expect(
normalizeOpenCodeEvent({
type: 'message.part.delta',
properties: {
sessionID: 'ses_1',
messageID: 'msg_1',
partID: 'part_1',
field: 'text',
delta: 'hello',
},
})
).toMatchObject({
kind: 'message_part_delta',
sessionId: 'ses_1',
messageId: 'msg_1',
partId: 'part_1',
field: 'text',
delta: 'hello',
});
});
it('normalizes permission events across v1.14 and legacy ids', () => {
expect(
normalizeOpenCodeEvent({
type: 'permission.asked',
properties: {
sessionID: 'ses_1',
id: 'perm_1',
},
})
).toMatchObject({
kind: 'permission_asked',
sessionId: 'ses_1',
requestId: 'perm_1',
});
expect(
normalizeOpenCodeEvent({
type: 'permission.replied',
properties: {
sessionID: 'ses_1',
requestID: 'perm_legacy',
},
})
).toMatchObject({
kind: 'permission_replied',
sessionId: 'ses_1',
requestId: 'perm_legacy',
});
});
it('returns unknown event instead of throwing on incomplete known payloads', () => {
expect(
normalizeOpenCodeEvent({
type: 'message.part.delta',
properties: {
sessionID: 'ses_1',
messageID: 'msg_1',
field: 'text',
delta: 'hello',
},
})
).toMatchObject({
kind: 'unknown',
type: 'message.part.delta',
});
});
it('maps normalized status and projections to durable session state', () => {
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
hasPendingPermission: true,
hasLatestAssistantError: false,
replyPendingSinceMessageId: null,
})
).toBe('blocked');
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
hasPendingPermission: false,
hasLatestAssistantError: true,
replyPendingSinceMessageId: null,
})
).toBe('error');
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'retry' }), {
hasPendingPermission: false,
hasLatestAssistantError: false,
replyPendingSinceMessageId: null,
})
).toBe('retrying');
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'busy' }), {
hasPendingPermission: false,
hasLatestAssistantError: false,
replyPendingSinceMessageId: null,
})
).toBe('running');
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'idle' }), {
hasPendingPermission: false,
hasLatestAssistantError: false,
replyPendingSinceMessageId: 'msg_1',
})
).toBe('reply_pending');
expect(
mapOpenCodeStatusToDurableState(normalizeOpenCodeSessionStatus({ type: 'idle' }), {
hasPendingPermission: false,
hasLatestAssistantError: false,
replyPendingSinceMessageId: null,
})
).toBe('idle');
});
});

View file

@ -0,0 +1,258 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
canMarkOpenCodeRunReady,
createOpenCodeLaunchEvidenceHash,
createOpenCodeLaunchTransactionStore,
redactOpenCodeLaunchEvidence,
type OpenCodeLaunchCheckpoint,
type OpenCodeLaunchTransaction,
} from '../../../../src/main/services/team/opencode/store/OpenCodeLaunchTransactionStore';
let tempDir: string;
let now: Date;
describe('OpenCodeLaunchTransactionStore', () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-launch-tx-'));
now = new Date('2026-04-21T12:00:00.000Z');
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('begins a run and blocks duplicate launch while active', async () => {
const store = createStore();
await expect(
store.beginRun({
teamName: 'team-a',
runId: 'run-1',
})
).resolves.toMatchObject({
state: 'created',
transaction: {
teamName: 'team-a',
runId: 'run-1',
status: 'active',
},
});
await expect(
store.beginRun({
teamName: 'team-a',
runId: 'run-2',
})
).resolves.toMatchObject({
state: 'already_active',
transaction: {
runId: 'run-1',
},
});
});
it('adds checkpoints idempotently and ignores late checkpoints from old runs', async () => {
const store = createStore();
await store.beginRun({ teamName: 'team-a', runId: 'run-1' });
const checkpoint = buildCheckpoint({
name: 'member_session_recorded',
memberName: 'Builder',
evidenceHash: createOpenCodeLaunchEvidenceHash({ sessionId: 'session-1' }),
});
await expect(store.addCheckpoint(checkpoint)).resolves.toBe('created');
await expect(store.addCheckpoint(checkpoint)).resolves.toBe('unchanged');
await expect(
store.hasCheckpoint({
teamName: 'team-a',
runId: 'run-1',
memberName: 'Builder',
name: 'member_session_recorded',
})
).resolves.toBe(true);
await expect(
store.addCheckpoint({
...checkpoint,
runId: 'old-run',
})
).rejects.toThrow('OpenCode launch transaction not found: old-run');
await expect(store.read('team-a', 'run-1')).resolves.toMatchObject({
checkpoints: [expect.objectContaining({ name: 'member_session_recorded' })],
});
});
it('finishes active transaction and rejects stale finish for another run', async () => {
const store = createStore();
await store.beginRun({ teamName: 'team-a', runId: 'run-1' });
await expect(
store.finish({
teamName: 'team-a',
runId: 'old-run',
status: 'failed',
})
).rejects.toThrow('OpenCode launch transaction old-run is stale; active run is run-1');
await expect(
store.finish({
teamName: 'team-a',
runId: 'run-1',
status: 'ready',
})
).resolves.toBe('finished');
await expect(store.readActive('team-a')).resolves.toBeNull();
await expect(store.read('team-a', 'run-1')).resolves.toMatchObject({
status: 'ready',
});
});
it('quarantines future or invalid transaction data through VersionedJsonStore', async () => {
const filePath = path.join(tempDir, 'launch-transactions.json');
await fs.writeFile(
filePath,
JSON.stringify({
schemaVersion: 1,
updatedAt: '2026-04-21T12:00:00.000Z',
data: [{ teamName: 'team-a', runId: '' }],
}),
'utf8'
);
const store = createStore(filePath);
await expect(store.list()).rejects.toMatchObject({
reason: 'invalid_data',
});
const files = await fs.readdir(tempDir);
expect(files.some((file) => file.includes('invalid_data'))).toBe(true);
});
});
describe('canMarkOpenCodeRunReady', () => {
it('lists exact missing readiness checkpoints before run_ready', () => {
const transaction = transactionWithCheckpoints([
buildCheckpoint({
name: 'member_session_recorded',
memberName: 'Builder',
}),
]);
expect(
canMarkOpenCodeRunReady({
members: [
{ name: 'Builder', launchState: 'confirmed_alive' },
{ name: 'Reviewer', launchState: 'pending' },
],
transaction,
toolProof: { ok: false },
deliveryReady: false,
})
).toEqual({
ok: false,
missing: [
'Builder:required_tools_proven',
'Reviewer:member_session_recorded',
'Reviewer:required_tools_proven',
'Reviewer:bootstrap_confirmed',
'required_runtime_tools',
'runtime_delivery_service',
],
});
});
it('allows ready only when every member and runtime delivery proof exists', () => {
const transaction = transactionWithCheckpoints([
buildCheckpoint({ name: 'member_session_recorded', memberName: 'Builder' }),
buildCheckpoint({ name: 'required_tools_proven', memberName: 'Builder' }),
buildCheckpoint({ name: 'member_session_recorded', memberName: 'Reviewer' }),
buildCheckpoint({ name: 'required_tools_proven', memberName: 'Reviewer' }),
]);
expect(
canMarkOpenCodeRunReady({
members: [
{ name: 'Builder', launchState: 'confirmed_alive' },
{ name: 'Reviewer', launchState: 'confirmed_alive' },
],
transaction,
toolProof: { ok: true },
deliveryReady: true,
})
).toEqual({
ok: true,
missing: [],
});
});
});
describe('OpenCode launch evidence redaction', () => {
it('redacts secret fields before hashing evidence', () => {
const evidence = {
sessionId: 'session-1',
token: 'live-token',
nested: {
apiKey: 'live-key',
},
};
expect(redactOpenCodeLaunchEvidence(evidence)).toEqual({
sessionId: 'session-1',
token: '[redacted]',
nested: {
apiKey: '[redacted]',
},
});
expect(createOpenCodeLaunchEvidenceHash(evidence)).toBe(
createOpenCodeLaunchEvidenceHash({
sessionId: 'session-1',
token: 'other-token',
nested: {
apiKey: 'other-key',
},
})
);
});
});
function createStore(filePath = path.join(tempDir, 'launch-transactions.json')) {
return createOpenCodeLaunchTransactionStore({
filePath,
clock: () => now,
});
}
function buildCheckpoint(
overrides: Partial<OpenCodeLaunchCheckpoint>
): OpenCodeLaunchCheckpoint {
return {
name: 'run_created',
teamName: 'team-a',
runId: 'run-1',
memberName: null,
runtimeSessionId: null,
hostKey: null,
evidenceHash: createOpenCodeLaunchEvidenceHash({ ok: true }),
createdAt: '2026-04-21T12:00:00.000Z',
diagnostics: [],
...overrides,
};
}
function transactionWithCheckpoints(
checkpoints: OpenCodeLaunchCheckpoint[]
): OpenCodeLaunchTransaction {
return {
teamName: 'team-a',
runId: 'run-1',
providerId: 'opencode',
startedAt: '2026-04-21T12:00:00.000Z',
updatedAt: '2026-04-21T12:00:00.000Z',
status: 'active',
checkpoints,
};
}

View file

@ -0,0 +1,158 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
assertManagedOverlayDoesNotShadowUserConfig,
buildManagedOverlayConfig,
OpenCodeBehaviorSourceScanner,
OpenCodeManagedOverlayBuilder,
pickAppOwnedMcpServerName,
} from '../../../../src/main/services/team/opencode/config/OpenCodeManagedOverlay';
describe('OpenCodeManagedOverlay', () => {
let tempDir: string;
let homePath: string;
let projectPath: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-managed-overlay-'));
homePath = path.join(tempDir, 'home');
projectPath = path.join(tempDir, 'project');
await fs.mkdir(projectPath, { recursive: true });
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('builds a minimal inline MCP overlay without user behavior keys or pure mode', async () => {
await writeJson(path.join(projectPath, 'opencode.json'), {
plugin: ['user-plugin'],
mcp: { user_server: { type: 'local', command: 'custom', enabled: true } },
});
const builder = new OpenCodeManagedOverlayBuilder(
new OpenCodeBehaviorSourceScanner({ homePath }),
() => new Date('2026-04-21T12:00:00.000Z')
);
const overlay = await builder.build({
projectPath,
preferredMcpName: 'agent-teams',
appMcpCommand: 'node',
appMcpArgs: ['server.js'],
appMcpEnv: { TEAM_RUNTIME: '1' },
});
const config = JSON.parse(overlay.env.OPENCODE_CONFIG_CONTENT);
expect(config).toEqual({
mcp: {
'agent-teams': {
type: 'local',
command: 'node',
args: ['server.js'],
enabled: true,
environment: { TEAM_RUNTIME: '1' },
timeout: 10_000,
},
},
});
expect(overlay.env).not.toHaveProperty('OPENCODE_PURE');
expect(overlay.env).not.toHaveProperty('OPENCODE_DISABLE_PROJECT_CONFIG');
expect(config).not.toHaveProperty('plugin');
expect(config).not.toHaveProperty('model');
expect(overlay.diagnostics).toContain('OpenCode managed overlay checked at 2026-04-21T12:00:00.000Z');
});
it('renames the app-owned MCP server when user config already declares the preferred name', async () => {
await writeJson(path.join(projectPath, 'opencode.json'), {
mcp: {
'agent-teams': { type: 'local', command: 'custom', enabled: true },
'agent-teams-runtime-1': { type: 'local', command: 'custom-2', enabled: true },
},
});
const builder = new OpenCodeManagedOverlayBuilder(new OpenCodeBehaviorSourceScanner({ homePath }));
const overlay = await builder.build({
projectPath,
preferredMcpName: 'agent-teams',
appMcpCommand: 'node',
appMcpArgs: ['server.js'],
appMcpEnv: {},
});
expect(overlay.appMcpServerName).toBe('agent-teams-runtime-2');
expect(overlay.diagnostics.join('\n')).toContain('already declares MCP server "agent-teams"');
});
it('reads JSONC project config and fingerprints plugin behavior sources', async () => {
await fs.mkdir(path.join(projectPath, '.opencode/plugins'), { recursive: true });
await fs.writeFile(
path.join(projectPath, 'opencode.jsonc'),
`{
// user-owned MCP must be observed, not overwritten
"mcp": {
"agent-teams": { "type": "local", "command": "custom", "enabled": true }
}
}`,
'utf8'
);
await fs.writeFile(path.join(projectPath, '.opencode/plugins/example.ts'), 'export default {}', 'utf8');
const scanner = new OpenCodeBehaviorSourceScanner({ homePath });
await expect(scanner.readDeclaredMcpNames(projectPath)).resolves.toEqual(new Set(['agent-teams']));
const sources = await scanner.scan(projectPath);
expect(sources).toContainEqual(
expect.objectContaining({
kind: 'project_plugin_dir',
exists: true,
fileCount: 1,
fingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
})
);
expect(sources).toContainEqual(
expect.objectContaining({
kind: 'project_opencode_dir',
exists: true,
fileCount: 1,
})
);
});
it('rejects managed overlays that would shadow user behavior keys', () => {
expect(() =>
assertManagedOverlayDoesNotShadowUserConfig({
...buildManagedOverlayConfig({
serverName: 'agent-teams',
command: 'node',
args: [],
environment: {},
timeout: 10_000,
}),
plugin: ['managed-plugin'],
model: 'managed-model',
})
).toThrow('Managed OpenCode overlay must not set user behavior keys: plugin, model');
});
it('picks deterministic collision-safe app MCP names', () => {
expect(pickAppOwnedMcpServerName('agent-teams', new Set())).toBe('agent-teams');
expect(pickAppOwnedMcpServerName('agent-teams', new Set(['agent-teams']))).toBe(
'agent-teams-runtime-1'
);
expect(
pickAppOwnedMcpServerName(
'agent-teams',
new Set(['agent-teams', 'agent-teams-runtime-1', 'agent-teams-runtime-2'])
)
).toBe('agent-teams-runtime-3');
});
});
async function writeJson(filePath: string, value: unknown): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}

View file

@ -0,0 +1,224 @@
import { describe, expect, it, vi } from 'vitest';
import {
APP_MCP_RUNTIME_TOOL_CONTRACTS,
assertRuntimeDeliverMessageSchema,
buildOpenCodeCanonicalMcpToolId,
matchRequiredOpenCodeTools,
OpenCodeMcpToolAvailabilityProbe,
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
sanitizeOpenCodeMcpToolPart,
verifyAppMcpRuntimeToolContracts,
type OpenCodeInfrastructureToolClient,
type OpenCodeToolListItem,
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
describe('OpenCode MCP tool availability', () => {
it('builds source-verified canonical MCP tool ids', () => {
expect(sanitizeOpenCodeMcpToolPart('agent-teams')).toBe('agent-teams');
expect(buildOpenCodeCanonicalMcpToolId('agent-teams', 'runtime_deliver_message')).toBe(
'agent-teams_runtime_deliver_message'
);
});
it('fails production proof when only alias ids are observed', () => {
const proof = matchRequiredOpenCodeTools({
route: '/experimental/tool/ids',
serverName: 'agent-teams',
requiredTools: ['runtime_deliver_message'],
observedTools: ['mcp__agent-teams__runtime_deliver_message'],
});
expect(proof).toMatchObject({
ok: false,
missingTools: ['runtime_deliver_message'],
matchedByRequiredTool: {
runtime_deliver_message: null,
},
aliasMatchedByRequiredTool: {
runtime_deliver_message: 'mcp__agent-teams__runtime_deliver_message',
},
});
expect(proof.diagnostics).toContain(
'OpenCode observed alias mcp__agent-teams__runtime_deliver_message but missing canonical app MCP tool id agent-teams_runtime_deliver_message'
);
});
it('proves required tools through experimental tool ids', async () => {
const client = fakeToolClient({
ids: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
),
tools: [],
});
const probe = new OpenCodeMcpToolAvailabilityProbe(client);
await expect(
probe.proveRequiredTools({
serverName: 'agent-teams',
requiredTools: [...REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS],
providerId: 'anthropic',
modelId: 'claude-sonnet',
})
).resolves.toMatchObject({
ok: true,
route: '/experimental/tool/ids',
missingTools: [],
matchedByRequiredTool: {
runtime_deliver_message: 'agent-teams_runtime_deliver_message',
},
});
expect(client.listExperimentalTools).not.toHaveBeenCalled();
});
it('falls back to provider/model tool definitions when ids route fails', async () => {
const client = fakeToolClient({
idsError: new Error('ids unavailable'),
tools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => ({
id: buildOpenCodeCanonicalMcpToolId('agent-teams', tool),
})),
});
const probe = new OpenCodeMcpToolAvailabilityProbe(client);
await expect(
probe.proveRequiredTools({
serverName: 'agent-teams',
requiredTools: [...REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS],
providerId: 'anthropic',
modelId: 'claude-sonnet',
})
).resolves.toMatchObject({
ok: true,
route: '/experimental/tool',
diagnostics: [],
});
expect(client.listExperimentalTools).toHaveBeenCalledWith({
providerId: 'anthropic',
modelId: 'claude-sonnet',
});
});
it('keeps launch blocked when neither tool endpoint proves canonical tools', async () => {
const client = fakeToolClient({
ids: ['agent-teams_runtime_bootstrap_checkin'],
toolsError: new Error('definitions unavailable'),
});
const probe = new OpenCodeMcpToolAvailabilityProbe(client);
const proof = await probe.proveRequiredTools({
serverName: 'agent-teams',
requiredTools: [...REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS],
providerId: 'anthropic',
modelId: 'claude-sonnet',
});
expect(proof.ok).toBe(false);
expect(proof.missingTools).toEqual(
expect.arrayContaining(['runtime_deliver_message', 'runtime_task_event', 'runtime_heartbeat'])
);
expect(proof.diagnostics).toContain(
'OpenCode app-owned MCP server is connected but required runtime tools were not proven available'
);
});
it('verifies direct app MCP runtime tool contracts', () => {
const result = verifyAppMcpRuntimeToolContracts(
APP_MCP_RUNTIME_TOOL_CONTRACTS.map((contract) => ({
name: contract.name,
inputSchema: schemaWithRequired(contract.requiredInputFields),
}))
);
expect(result).toEqual({
ok: true,
observedToolNames: [
'runtime_bootstrap_checkin',
'runtime_deliver_message',
'runtime_heartbeat',
'runtime_task_event',
],
diagnostics: [],
});
});
it('fails direct app MCP preflight when delivery schema misses idempotencyKey', () => {
const tools = APP_MCP_RUNTIME_TOOL_CONTRACTS.map((contract) => ({
name: contract.name,
inputSchema:
contract.name === 'runtime_deliver_message'
? schemaWithRequired(['runId', 'teamName', 'fromMemberName', 'runtimeSessionId', 'to', 'text'])
: schemaWithRequired(contract.requiredInputFields),
}));
expect(verifyAppMcpRuntimeToolContracts(tools).diagnostics).toContain(
'App MCP tool runtime_deliver_message missing required field idempotencyKey'
);
});
it('validates provider/model runtime_deliver_message tool schema', () => {
const tools: OpenCodeToolListItem[] = [
{
id: 'agent-teams_runtime_deliver_message',
parameters: schemaWithRequired([
'idempotencyKey',
'runId',
'teamName',
'fromMemberName',
'runtimeSessionId',
'to',
'text',
]),
},
];
expect(assertRuntimeDeliverMessageSchema(tools)).toEqual([]);
expect(
assertRuntimeDeliverMessageSchema([
{
id: 'agent-teams_runtime_deliver_message',
parameters: schemaWithRequired(['runId', 'teamName']),
},
])
).toEqual([
{
severity: 'error',
message:
'runtime_deliver_message schema missing required fields: idempotencyKey, fromMemberName, runtimeSessionId, to, text',
missingFields: ['idempotencyKey', 'fromMemberName', 'runtimeSessionId', 'to', 'text'],
},
]);
});
});
function fakeToolClient(options: {
ids?: string[];
tools?: OpenCodeToolListItem[];
idsError?: Error;
toolsError?: Error;
}): OpenCodeInfrastructureToolClient & {
listExperimentalToolIds: ReturnType<typeof vi.fn>;
listExperimentalTools: ReturnType<typeof vi.fn>;
} {
return {
listExperimentalToolIds: vi.fn(async () => {
if (options.idsError) {
throw options.idsError;
}
return options.ids ?? [];
}),
listExperimentalTools: vi.fn(async () => {
if (options.toolsError) {
throw options.toolsError;
}
return options.tools ?? [];
}),
};
}
function schemaWithRequired(required: string[]): Record<string, unknown> {
return {
type: 'object',
required,
properties: Object.fromEntries(required.map((field) => [field, { type: 'string' }])),
};
}

View file

@ -0,0 +1,209 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
assertOpenCodeProductionE2EArtifactGate,
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
validateOpenCodeProductionE2EEvidence,
type OpenCodeProductionE2EEvidence,
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
import { OpenCodeProductionE2EEvidenceStore } from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore';
import {
buildOpenCodeCanonicalMcpToolId,
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
describe('OpenCodeProductionE2EEvidence', () => {
let tempDir: string;
const now = new Date('2026-04-21T12:00:00.000Z');
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-production-e2e-'));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('accepts strict evidence only when runtime identity, model and required MCP tools match', () => {
const evidence = passingEvidence();
expect(validateOpenCodeProductionE2EEvidence(evidence)).toEqual(evidence);
expect(
assertOpenCodeProductionE2EArtifactGate({
evidence,
artifactPath: '/tmp/opencode-e2e',
now,
expected: {
opencodeVersion: '1.14.19',
binaryFingerprint: 'version:1.14.19',
capabilitySnapshotId: 'cap-1',
selectedModel: 'openai/gpt-5.4-mini',
requiredMcpTools: ['agent-teams_runtime_deliver_message'],
},
})
).toEqual({
ok: true,
diagnostics: [],
});
});
it('fails closed for stale, mismatched or incomplete evidence', () => {
const expired = passingEvidence({
expiresAt: '2026-04-21T11:59:59.000Z',
selectedModel: 'openrouter/anthropic/claude-sonnet-4.5',
requiredSignals: requiredSignals({ stale_run_rejected: false }),
mcpTools: {
requiredTools: ['agent-teams_runtime_deliver_message'],
observedTools: [],
},
});
expect(
assertOpenCodeProductionE2EArtifactGate({
evidence: expired,
artifactPath: '/tmp/opencode-e2e',
now,
expected: {
opencodeVersion: '1.14.19',
binaryFingerprint: 'version:1.14.19',
capabilitySnapshotId: 'cap-1',
selectedModel: 'openai/gpt-5.4-mini',
requiredMcpTools: ['agent-teams_runtime_deliver_message'],
},
})
).toMatchObject({
ok: false,
diagnostics: expect.arrayContaining([
'OpenCode production E2E evidence is expired',
'OpenCode production E2E evidence is missing signals: stale_run_rejected',
'OpenCode production E2E evidence is missing observed MCP tools: agent-teams_runtime_deliver_message',
'OpenCode production E2E evidence model openrouter/anthropic/claude-sonnet-4.5 does not match selected model openai/gpt-5.4-mini',
]),
});
});
it('reads missing evidence as a production-blocking diagnostic and quarantines corrupt artifacts', async () => {
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
const store = new OpenCodeProductionE2EEvidenceStore({
filePath,
clock: () => now,
});
await expect(store.read()).resolves.toMatchObject({
ok: true,
evidence: null,
artifactPath: filePath,
diagnostics: ['OpenCode production E2E evidence artifact has not been written yet'],
});
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, '{broken', 'utf8');
const corrupt = await store.read();
expect(corrupt).toMatchObject({
ok: false,
evidence: null,
artifactPath: filePath,
});
expect(corrupt.diagnostics[0]).toContain(
'OpenCode production E2E evidence store is unreadable'
);
});
it('writes evidence with the store path as artifactPath when the input omits it', async () => {
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
const store = new OpenCodeProductionE2EEvidenceStore({
filePath,
clock: () => now,
});
await store.write({
...passingEvidence(),
artifactPath: null,
});
await expect(store.read()).resolves.toMatchObject({
ok: true,
evidence: {
artifactPath: filePath,
evidenceId: 'e2e-1',
},
diagnostics: [],
});
});
});
function passingEvidence(
overrides: Partial<OpenCodeProductionE2EEvidence> = {}
): OpenCodeProductionE2EEvidence {
const createdAt = '2026-04-21T12:00:00.000Z';
const sessionId = 'session-1';
const requiredToolIds = REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
);
return {
schemaVersion: 1,
evidenceId: 'e2e-1',
createdAt,
expiresAt: '2026-04-21T12:10:00.000Z',
version: '1.14.19',
passed: true,
artifactPath: '/tmp/opencode-e2e',
binaryFingerprint: 'version:1.14.19',
capabilitySnapshotId: 'cap-1',
selectedModel: 'openai/gpt-5.4-mini',
projectPathFingerprint: 'project-a',
requiredSignals: requiredSignals(),
mcpTools: {
requiredTools: requiredToolIds,
observedTools: requiredToolIds,
},
launch: {
runId: 'run-1',
teamId: 'team-a',
teamLaunchState: 'ready',
memberCount: 1,
sessions: [
{
memberName: 'Dev',
sessionId,
launchState: 'confirmed_alive',
},
],
durableCheckpoints: OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.map((name) => ({
name,
observedAt: createdAt,
})),
},
reconcile: {
runId: 'run-1',
teamLaunchState: 'ready',
memberCount: 1,
},
stop: {
runId: 'run-1',
stopped: true,
stoppedSessionIds: [sessionId],
},
logProjection: {
observed: true,
projectedMessageCount: 1,
},
...overrides,
};
}
function requiredSignals(
overrides: Partial<
Record<(typeof OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS)[number], boolean>
> = {}
) {
return Object.fromEntries(
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [signal, overrides[signal] ?? true])
) as OpenCodeProductionE2EEvidence['requiredSignals'];
}

View file

@ -0,0 +1,427 @@
import { constants as fsConstants, promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { OpenCodeBridgeCommandClient } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
import {
createOpenCodeBridgeCommandLeaseStore,
createOpenCodeBridgeCommandLedgerStore,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore';
import {
createOpenCodeBridgeClientIdentity,
OpenCodeBridgeCommandHandshakePort,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeHandshakeClient';
import { OpenCodeReadinessBridge } from '../../../../src/main/services/team/opencode/bridge/OpenCodeReadinessBridge';
import { OpenCodeStateChangingBridgeCommandService } from '../../../../src/main/services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
import {
assertOpenCodeProductionE2EArtifactGate,
OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION,
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
type OpenCodeProductionE2EEvidence,
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
import {
buildOpenCodeCanonicalMcpToolId,
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import { resolveAgentTeamsMcpLaunchSpec } from '../../../../src/main/services/team/TeamMcpConfigBuilder';
import type {
OpenCodeBridgeRuntimeSnapshot,
OpenCodeLaunchTeamCommandData,
OpenCodeStopTeamCommandData,
RuntimeStoreManifestEvidence,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../../../../src/main/services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
import type { OpenCodeBridgeCommandExecutor } from '../../../../src/main/services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
const liveDescribe = process.env.OPENCODE_E2E === '1' ? describe : describe.skip;
const PROJECT_PATH = '/Users/belief/dev/projects/claude/claude_team_opencode_integration';
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
const DEFAULT_MODEL = 'opencode/big-pickle';
liveDescribe('OpenCode production gate live e2e', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-production-gate-e2e-'));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('runs live launch/reconcile/transcript/stop and accepts production evidence with app MCP tool proof', async () => {
const selectedModel = process.env.OPENCODE_E2E_MODEL?.trim() || DEFAULT_MODEL;
const orchestratorCli =
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
await assertExecutable(orchestratorCli);
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
const bridgeEnv = {
...createStableBridgeEnv(),
PATH: withBunOnPath(process.env.PATH ?? ''),
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
};
const bridgeClient = new OpenCodeBridgeCommandClient({
binaryPath: orchestratorCli,
tempDirectory: path.join(tempDir, 'bridge-input'),
env: bridgeEnv,
});
const stateChangingCommands = createStateChangingCommands({
bridge: bridgeClient,
controlDir: path.join(tempDir, 'control'),
});
const readinessBridge = new OpenCodeReadinessBridge(bridgeClient, {
stateChangingCommands,
timeoutMs: 180_000,
launchTimeoutMs: 180_000,
reconcileTimeoutMs: 90_000,
stopTimeoutMs: 90_000,
});
const readiness = await readinessBridge.checkOpenCodeTeamLaunchReadiness({
projectPath: PROJECT_PATH,
selectedModel,
requireExecutionProbe: false,
});
const runtime = readinessBridge.getLastOpenCodeRuntimeSnapshot(PROJECT_PATH);
if (!runtime) {
throw new Error(
`OpenCode live readiness did not return runtime snapshot: ${[
...readiness.diagnostics,
...readiness.missing,
].join('; ')}`
);
}
expect(runtime?.version).toBe('1.14.19');
expect(runtime?.capabilitySnapshotId).toBeTruthy();
const runId = `opencode-e2e-${Date.now()}`;
const teamName = `opencode-e2e-team-${Date.now()}`;
const memberName = 'E2E';
let launch: OpenCodeLaunchTeamCommandData | null = null;
let reconcile: OpenCodeLaunchTeamCommandData | null = null;
let stop: OpenCodeStopTeamCommandData | null = null;
let transcriptMessages = 0;
let staleRunRejected = false;
try {
launch = await readinessBridge.launchOpenCodeTeam({
mode: 'dogfood',
runId,
teamId: teamName,
teamName,
projectPath: PROJECT_PATH,
selectedModel,
members: [
{
name: memberName,
role: 'e2e',
prompt: 'Reply with exactly: opencode-production-gate-e2e',
},
],
leadPrompt: 'Live OpenCode production gate e2e',
expectedCapabilitySnapshotId: runtime?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
});
expect(launch.teamLaunchState).toBe('ready');
expect(launch.members[memberName]?.launchState).toBe('confirmed_alive');
reconcile = await readinessBridge.reconcileOpenCodeTeam({
runId,
teamId: teamName,
teamName,
projectPath: PROJECT_PATH,
expectedCapabilitySnapshotId: runtime?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
expectedMembers: [{ name: memberName, model: selectedModel }],
reason: 'production_gate_e2e',
});
expect(reconcile.teamLaunchState).toBe('ready');
const transcript = await bridgeClient.execute<
{ teamId: string; teamName: string; memberName: string },
{ logProjection?: { messages?: unknown[] }; messages?: unknown[] }
>(
'opencode.getRuntimeTranscript',
{ teamId: teamName, teamName, memberName },
{ cwd: PROJECT_PATH, timeoutMs: 60_000 }
);
expect(transcript.ok).toBe(true);
if (transcript.ok) {
transcriptMessages =
transcript.data.logProjection?.messages?.length ?? transcript.data.messages?.length ?? 0;
expect(transcriptMessages).toBeGreaterThan(0);
}
staleRunRejected = await rejectsStaleCapability({
stateChangingCommands,
teamName,
runId: `${runId}-stale`,
selectedModel,
});
stop = await readinessBridge.stopOpenCodeTeam({
runId,
teamId: teamName,
teamName,
projectPath: PROJECT_PATH,
expectedCapabilitySnapshotId: runtime?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
reason: 'production_gate_e2e_cleanup',
force: true,
});
expect(stop.stopped).toBe(true);
const candidate = buildCandidateEvidence({
runId,
teamName,
memberName,
selectedModel,
runtime: runtime!,
readinessObservedTools: readiness.evidence.observedMcpTools,
launch,
reconcile,
stop,
transcriptMessages,
staleRunRejected,
appMcpToolsVisible: readiness.requiredToolsPresent,
});
const gate = assertOpenCodeProductionE2EArtifactGate({
evidence: candidate,
artifactPath: candidate.artifactPath,
expected: {
opencodeVersion: runtime?.version ?? null,
binaryFingerprint: runtime?.binaryFingerprint ?? null,
capabilitySnapshotId: runtime?.capabilitySnapshotId ?? null,
selectedModel,
requiredMcpTools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
),
},
});
expect(gate).toEqual({
ok: true,
diagnostics: [],
});
} finally {
if (!stop) {
await readinessBridge
.stopOpenCodeTeam({
runId,
teamId: teamName,
teamName,
projectPath: PROJECT_PATH,
expectedCapabilitySnapshotId: runtime?.capabilitySnapshotId ?? null,
manifestHighWatermark: null,
reason: 'production_gate_e2e_finally_cleanup',
force: true,
})
.catch(() => undefined);
}
}
}, 240_000);
});
function createStateChangingCommands(input: {
bridge: OpenCodeBridgeCommandExecutor;
controlDir: string;
}): OpenCodeStateChangingBridgeCommandService {
const clientIdentity = createOpenCodeBridgeClientIdentity({
appVersion: '1.3.0-e2e',
gitSha: null,
buildId: 'opencode-production-gate-e2e',
});
return new OpenCodeStateChangingBridgeCommandService({
expectedClientIdentity: clientIdentity,
handshakePort: new OpenCodeBridgeCommandHandshakePort({
bridge: input.bridge,
clientIdentity,
}),
leaseStore: createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(input.controlDir, 'leases.json'),
}),
ledger: createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(input.controlDir, 'ledger.json'),
}),
bridge: input.bridge,
manifestReader: new StaticManifestReader(),
});
}
class StaticManifestReader implements RuntimeStoreManifestReader {
async read(): Promise<RuntimeStoreManifestEvidence> {
return {
highWatermark: 0,
activeRunId: null,
capabilitySnapshotId: null,
};
}
}
async function rejectsStaleCapability(input: {
stateChangingCommands: OpenCodeStateChangingBridgeCommandService;
teamName: string;
runId: string;
selectedModel: string;
}): Promise<boolean> {
try {
await input.stateChangingCommands.execute({
command: 'opencode.reconcileTeam',
teamName: input.teamName,
runId: input.runId,
capabilitySnapshotId: 'opencode:stale-capability',
behaviorFingerprint: null,
body: {
runId: input.runId,
teamId: input.teamName,
teamName: input.teamName,
projectPath: PROJECT_PATH,
expectedCapabilitySnapshotId: 'opencode:stale-capability',
manifestHighWatermark: null,
expectedMembers: [{ name: 'E2E', model: input.selectedModel }],
reason: 'production_gate_stale_run_probe',
},
cwd: PROJECT_PATH,
timeoutMs: 30_000,
});
return false;
} catch (error) {
return error instanceof Error && error.message.includes('capability snapshot mismatch');
}
}
function buildCandidateEvidence(input: {
runId: string;
teamName: string;
memberName: string;
selectedModel: string;
runtime: OpenCodeBridgeRuntimeSnapshot;
readinessObservedTools: string[];
launch: OpenCodeLaunchTeamCommandData;
reconcile: OpenCodeLaunchTeamCommandData;
stop: OpenCodeStopTeamCommandData;
transcriptMessages: number;
staleRunRejected: boolean;
appMcpToolsVisible: boolean;
}): OpenCodeProductionE2EEvidence {
const now = new Date();
const createdAt = now.toISOString();
const sessionId = input.launch.members[input.memberName]?.sessionId ?? 'missing-session';
const checkpointByName = new Map<string, { name: string; observedAt: string }>();
for (const checkpoint of input.launch.durableCheckpoints ?? []) {
checkpointByName.set(checkpoint.name, {
name: checkpoint.name,
observedAt: checkpoint.observedAt,
});
}
for (const evidence of input.launch.members[input.memberName]?.evidence ?? []) {
checkpointByName.set(evidence.kind, {
name: evidence.kind,
observedAt: evidence.observedAt,
});
}
for (const name of OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS) {
checkpointByName.set(name, checkpointByName.get(name) ?? { name, observedAt: createdAt });
}
return {
schemaVersion: OPENCODE_PRODUCTION_E2E_EVIDENCE_SCHEMA_VERSION,
evidenceId: `live-${input.runId}`,
createdAt,
expiresAt: new Date(now.getTime() + 60 * 60 * 1000).toISOString(),
version: input.runtime.version ?? 'unknown',
passed: true,
artifactPath: path.join(os.tmpdir(), `opencode-production-e2e-${input.runId}.json`),
binaryFingerprint: input.runtime.binaryFingerprint ?? 'unknown',
capabilitySnapshotId: input.runtime.capabilitySnapshotId ?? 'unknown',
selectedModel: input.selectedModel,
projectPathFingerprint: null,
requiredSignals: {
...Object.fromEntries(
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [signal, true])
),
app_mcp_tools_visible: input.appMcpToolsVisible,
stale_run_rejected: input.staleRunRejected,
} as OpenCodeProductionE2EEvidence['requiredSignals'],
mcpTools: {
requiredTools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
),
observedTools: input.readinessObservedTools,
},
launch: {
runId: input.runId,
teamId: input.teamName,
teamLaunchState: 'ready',
memberCount: 1,
sessions: [
{
memberName: input.memberName,
sessionId,
launchState: 'confirmed_alive',
},
],
durableCheckpoints: Array.from(checkpointByName.values()),
},
reconcile: {
runId: input.reconcile.runId,
teamLaunchState: 'ready',
memberCount: Object.keys(input.reconcile.members).length,
},
stop: {
runId: input.stop.runId,
stopped: true,
stoppedSessionIds: Object.values(input.stop.members)
.map((member) => member.sessionId)
.filter((value): value is string => Boolean(value)),
},
logProjection: {
observed: true,
projectedMessageCount: input.transcriptMessages,
},
};
}
async function assertExecutable(filePath: string): Promise<void> {
await fs.access(filePath, fsConstants.X_OK);
}
function withBunOnPath(pathValue: string): string {
const bunDir = '/Users/belief/.bun/bin';
return pathValue.split(path.delimiter).includes(bunDir)
? pathValue
: `${bunDir}${path.delimiter}${pathValue}`;
}
function createStableBridgeEnv(): NodeJS.ProcessEnv {
const realHome = os.userInfo().homedir;
const passThroughKeys = [
'USER',
'LOGNAME',
'SHELL',
'TMPDIR',
'LANG',
'LC_ALL',
'OPENCODE_E2E_MODEL',
'OPENCODE_DISABLE_CLAUDE_CODE',
'OPENCODE_DISABLE_AUTOUPDATE',
];
return {
...Object.fromEntries(
passThroughKeys
.map((key) => [key, process.env[key]] as const)
.filter((entry): entry is readonly [string, string] => typeof entry[1] === 'string')
),
HOME: realHome,
USERPROFILE: realHome,
};
}

View file

@ -0,0 +1,405 @@
import { describe, expect, it, vi } from 'vitest';
import {
OpenCodeReadinessBridge,
type OpenCodeReadinessBridgeCommandExecutor,
type OpenCodeProductionE2EEvidenceReadPort,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeReadinessBridge';
import {
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
type OpenCodeProductionE2EEvidence,
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
import {
buildOpenCodeCanonicalMcpToolId,
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import type { OpenCodeTeamLaunchReadiness } from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import type {
OpenCodeBridgeFailureKind,
OpenCodeBridgeCommandName,
OpenCodeBridgeResult,
OpenCodeBridgeSuccess,
OpenCodeLaunchTeamCommandData,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
describe('OpenCodeReadinessBridge', () => {
it('executes the read-only opencode.readiness command and returns readiness data', async () => {
const readinessResult = readiness({ state: 'ready', launchAllowed: true });
const executor = fakeExecutor(bridgeSuccess(readinessResult));
const bridge = new OpenCodeReadinessBridge(executor, { timeoutMs: 15_000 });
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
})
).resolves.toBe(readinessResult);
expect(executor.execute).toHaveBeenCalledWith(
'opencode.readiness',
{
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
},
{
cwd: '/repo',
timeoutMs: 15_000,
}
);
expect(bridge.getLastOpenCodeRuntimeSnapshot('/repo')).toMatchObject({
capabilitySnapshotId: 'cap-1',
version: '1.14.19',
});
});
it('maps bridge failures into fail-closed readiness', async () => {
const executor = fakeExecutor(
bridgeFailure('timeout', 'OpenCode readiness command timed out', [
{
id: 'diag-1',
type: 'opencode_bridge_unknown_outcome',
providerId: 'opencode',
severity: 'warning',
message: 'timed out',
createdAt: '2026-04-21T12:00:00.000Z',
},
])
);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: false,
})
).resolves.toMatchObject({
state: 'unknown_error',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
hostHealthy: false,
requiredToolsPresent: false,
missing: ['OpenCode readiness command timed out'],
diagnostics: [
'OpenCode readiness bridge failed: timeout: OpenCode readiness command timed out',
'opencode_bridge_unknown_outcome: timed out',
],
});
expect(bridge.getLastOpenCodeRuntimeSnapshot('/repo')).toBeNull();
});
it('blocks production readiness when strict production E2E evidence is missing', async () => {
const executor = fakeExecutor(
bridgeSuccess(readiness({ state: 'ready', launchAllowed: true }))
);
const evidence = fakeEvidenceStore(null);
const bridge = new OpenCodeReadinessBridge(executor, { productionE2eEvidence: evidence });
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
launchMode: 'production',
})
).resolves.toMatchObject({
state: 'e2e_missing',
launchAllowed: false,
supportLevel: 'supported_e2e_pending',
missing: ['OpenCode production launch requires a current production E2E evidence artifact'],
diagnostics: [
'OpenCode production launch requires a current production E2E evidence artifact',
],
});
expect(evidence.read).toHaveBeenCalledOnce();
});
it('allows dogfood readiness while surfacing missing production E2E evidence diagnostics', async () => {
const executor = fakeExecutor(
bridgeSuccess(readiness({ state: 'ready', launchAllowed: true }))
);
const bridge = new OpenCodeReadinessBridge(executor, {
productionE2eEvidence: fakeEvidenceStore(null),
});
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
launchMode: 'dogfood',
})
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
supportLevel: 'supported_e2e_pending',
diagnostics: [
'OpenCode production launch requires a current production E2E evidence artifact',
],
});
});
it('keeps production readiness open when evidence matches runtime identity and raw model', async () => {
const executor = fakeExecutor(
bridgeSuccess(readiness({ state: 'ready', launchAllowed: true }))
);
const bridge = new OpenCodeReadinessBridge(executor, {
productionE2eEvidence: fakeEvidenceStore(productionEvidence()),
});
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
launchMode: 'production',
})
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
supportLevel: 'production_supported',
diagnostics: [],
});
});
it('routes state-changing launch commands through the guarded command service when configured', async () => {
const executor = fakeExecutor(
bridgeFailure('internal_error', 'direct bridge must not run', [])
);
const stateChangingExecute = vi.fn();
const stateChangingCommands = {
async execute<TBody, TData>(input: {
command: OpenCodeBridgeCommandName;
body: TBody;
}): Promise<OpenCodeBridgeResult<TData>> {
stateChangingExecute(input);
return bridgeCommandSuccess<OpenCodeLaunchTeamCommandData>({
command: input.command,
requestId: 'guarded-req-1',
data: {
runId: 'run-1',
teamLaunchState: 'ready',
members: {},
warnings: [],
diagnostics: [],
idempotencyKey: 'idem-1',
runtimeStoreManifestHighWatermark: 0,
},
}) as unknown as OpenCodeBridgeResult<TData>;
},
};
const bridge = new OpenCodeReadinessBridge(executor, { stateChangingCommands });
await expect(
bridge.launchOpenCodeTeam({
mode: 'dogfood',
runId: 'run-1',
teamId: 'team-a',
teamName: 'team-a',
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
members: [],
leadPrompt: '',
expectedCapabilitySnapshotId: 'cap-1',
manifestHighWatermark: 0,
})
).resolves.toMatchObject({
runId: 'run-1',
teamLaunchState: 'ready',
idempotencyKey: 'idem-1',
});
expect(stateChangingExecute).toHaveBeenCalledWith(
expect.objectContaining({
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
capabilitySnapshotId: 'cap-1',
cwd: '/repo',
})
);
expect(executor.execute).not.toHaveBeenCalled();
});
});
function fakeExecutor(
result: OpenCodeBridgeResult<unknown>
): OpenCodeReadinessBridgeCommandExecutor {
return {
execute: vi.fn(async () => result) as OpenCodeReadinessBridgeCommandExecutor['execute'],
};
}
function fakeEvidenceStore(
evidence: OpenCodeProductionE2EEvidence | null
): OpenCodeProductionE2EEvidenceReadPort & { read: ReturnType<typeof vi.fn> } {
return {
read: vi.fn(async () => ({
ok: true,
evidence,
artifactPath: '/tmp/opencode-production-e2e.json',
diagnostics: [],
})),
};
}
function bridgeSuccess(
data: OpenCodeTeamLaunchReadiness
): OpenCodeBridgeSuccess<OpenCodeTeamLaunchReadiness> {
return {
ok: true,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.readiness',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data,
};
}
function bridgeFailure(
kind: OpenCodeBridgeFailureKind,
message: string,
diagnostics: OpenCodeBridgeResult<unknown>['diagnostics']
): OpenCodeBridgeResult<unknown> {
return {
ok: false,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.readiness',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
error: {
kind,
message,
retryable: true,
},
diagnostics,
};
}
function bridgeCommandSuccess<TData>(input: {
command: OpenCodeBridgeCommandName;
requestId: string;
data: TData;
}): OpenCodeBridgeSuccess<TData> {
return {
ok: true,
schemaVersion: 1,
requestId: input.requestId,
command: input.command,
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data: input.data,
};
}
function readiness(
overrides: Partial<OpenCodeTeamLaunchReadiness> = {}
): OpenCodeTeamLaunchReadiness {
return {
state: 'adapter_disabled',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
opencodeVersion: '1.14.19',
installMethod: 'brew',
binaryPath: '/opt/homebrew/bin/opencode',
hostHealthy: true,
appMcpConnected: true,
requiredToolsPresent: true,
permissionBridgeReady: true,
runtimeStoresReady: true,
supportLevel: 'production_supported',
missing: [],
diagnostics: [],
evidence: {
capabilitiesReady: true,
mcpToolProofRoute: '/experimental/tool/ids',
observedMcpTools: ['agent-teams_runtime_deliver_message'],
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
...overrides,
};
}
function productionEvidence(
overrides: Partial<OpenCodeProductionE2EEvidence> = {}
): OpenCodeProductionE2EEvidence {
const createdAt = new Date().toISOString();
const sessionId = 'session-1';
const requiredToolIds = REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
);
return {
schemaVersion: 1,
evidenceId: 'e2e-1',
createdAt,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
version: '1.14.19',
passed: true,
artifactPath: '/tmp/opencode-production-e2e.json',
binaryFingerprint: 'bin-1',
capabilitySnapshotId: 'cap-1',
selectedModel: 'openai/gpt-5.4-mini',
projectPathFingerprint: 'project-a',
requiredSignals: Object.fromEntries(
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [signal, true])
) as OpenCodeProductionE2EEvidence['requiredSignals'],
mcpTools: {
requiredTools: requiredToolIds,
observedTools: requiredToolIds,
},
launch: {
runId: 'run-1',
teamId: 'team-a',
teamLaunchState: 'ready',
memberCount: 1,
sessions: [
{
memberName: 'Dev',
sessionId,
launchState: 'confirmed_alive',
},
],
durableCheckpoints: OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.map((name) => ({
name,
observedAt: createdAt,
})),
},
reconcile: {
runId: 'run-1',
teamLaunchState: 'ready',
memberCount: 1,
},
stop: {
runId: 'run-1',
stopped: true,
stoppedSessionIds: [sessionId],
},
logProjection: {
observed: true,
projectedMessageCount: 1,
},
...overrides,
};
}

View file

@ -0,0 +1,368 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
createOpenCodeBridgeHandshakeIdentityHash,
type OpenCodeBridgeCommandName,
type OpenCodeBridgeHandshake,
type OpenCodeBridgePeerIdentity,
type OpenCodeBridgeResult,
type OpenCodeBridgeSuccess,
type RuntimeStoreManifestEvidence,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import {
createOpenCodeBridgeCommandLeaseStore,
createOpenCodeBridgeCommandLedgerStore,
type OpenCodeBridgeCommandLedger,
type OpenCodeBridgeCommandLeaseStore,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore';
import {
OpenCodeStateChangingBridgeCommandService,
type OpenCodeBridgeCommandExecutor,
type OpenCodeBridgeHandshakePort,
type OpenCodeStateChangingBridgeDiagnosticsSink,
type RuntimeStoreManifestReader,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
describe('OpenCodeStateChangingBridgeCommandService', () => {
let tempDir: string;
let now: Date;
let nextLeaseId: number;
let ledger: OpenCodeBridgeCommandLedger;
let leaseStore: OpenCodeBridgeCommandLeaseStore;
let bridge: FakeBridgeExecutor;
let handshakePort: FakeHandshakePort;
let manifestReader: FakeManifestReader;
let diagnostics: FakeDiagnosticsSink;
let clientIdentity: OpenCodeBridgePeerIdentity;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-state-bridge-'));
now = new Date('2026-04-21T12:00:00.000Z');
nextLeaseId = 1;
ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
leaseStore = createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(tempDir, 'leases.json'),
idFactory: () => `lease-${nextLeaseId++}`,
clock: () => now,
});
clientIdentity = peerIdentity('claude_team');
handshakePort = new FakeHandshakePort(buildHandshake({
client: clientIdentity,
server: peerIdentity('agent_teams_orchestrator'),
}));
manifestReader = new FakeManifestReader();
bridge = new FakeBridgeExecutor();
diagnostics = new FakeDiagnosticsSink();
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('rejects state-changing command when bridge handshake has stale manifest high watermark', async () => {
handshakePort.nextHandshake = buildHandshake({
client: clientIdentity,
server: peerIdentity('agent_teams_orchestrator', {
runtimeStoreManifestHighWatermark: 9,
}),
});
const service = createService();
await expect(service.execute(buildLaunchInput())).rejects.toThrow(
'Bridge server runtime manifest high watermark is stale'
);
expect(bridge.calls).toHaveLength(0);
await expect(ledger.list()).resolves.toEqual([]);
await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
});
it('adds preconditions, commits ledger, and releases lease on success', async () => {
bridge.resultFactory = ({ body, options }) =>
bridgeSuccess({
requestId: options.requestId,
data: {
runId: 'run-1',
idempotencyKey: body.preconditions.idempotencyKey,
runtimeStoreManifestHighWatermark: 10,
},
});
const service = createService();
const result = await service.execute(buildLaunchInput());
expect(result.ok).toBe(true);
expect(bridge.calls).toHaveLength(1);
expect(bridge.calls[0].options).toMatchObject({ requestId: 'cmd-1' });
expect(bridge.calls[0].body).toMatchObject({
prompt: 'launch',
preconditions: {
handshakeIdentityHash: handshakePort.nextHandshake.identityHash,
expectedRunId: 'run-1',
expectedCapabilitySnapshotId: 'cap-1',
expectedBehaviorFingerprint: 'behavior-1',
expectedManifestHighWatermark: 10,
commandLeaseId: 'lease-1',
idempotencyKey: expect.stringMatching(/^opencode:opencode.launchTeam:team-a:run-1:/),
},
});
await expect(ledger.getByIdempotencyKey(bridge.calls[0].body.preconditions.idempotencyKey))
.resolves.toMatchObject({
requestId: 'cmd-1',
status: 'completed',
retryable: false,
completedAt: '2026-04-21T12:00:00.000Z',
});
await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
});
it('records unknown outcome after timeout and blocks retry before a duplicate bridge call', async () => {
bridge.resultFactory = ({ body, command, options }) => ({
ok: false,
schemaVersion: 1,
requestId: options.requestId,
command,
completedAt: '2026-04-21T12:00:10.000Z',
durationMs: 10_000,
error: {
kind: 'timeout',
message: 'timeout',
retryable: true,
},
diagnostics: [],
data: body,
} as OpenCodeBridgeResult<unknown>);
const service = createService();
const first = await service.execute(buildLaunchInput());
expect(first).toMatchObject({
ok: false,
error: { kind: 'timeout' },
});
const idempotencyKey = bridge.calls[0].body.preconditions.idempotencyKey;
await expect(ledger.getByIdempotencyKey(idempotencyKey)).resolves.toMatchObject({
status: 'unknown_after_timeout',
retryable: false,
lastError: 'timeout',
});
expect(diagnostics.append).toHaveBeenCalledWith(
expect.objectContaining({
type: 'opencode_bridge_unknown_outcome',
data: expect.objectContaining({
idempotencyKey,
leaseId: 'lease-1',
}),
})
);
await expect(service.execute(buildLaunchInput())).rejects.toThrow(
'OpenCode bridge command outcome must be reconciled before retry'
);
expect(bridge.calls).toHaveLength(1);
await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
});
it('marks result precondition mismatch as failed and does not leave active lease', async () => {
bridge.resultFactory = ({ body, options }) =>
bridgeSuccess({
requestId: options.requestId,
data: {
runId: 'run-1',
idempotencyKey: body.preconditions.idempotencyKey,
runtimeStoreManifestHighWatermark: 9,
},
});
const service = createService();
await expect(service.execute(buildLaunchInput())).rejects.toThrow(
'Bridge result manifest high watermark is stale'
);
const idempotencyKey = bridge.calls[0].body.preconditions.idempotencyKey;
await expect(ledger.getByIdempotencyKey(idempotencyKey)).resolves.toMatchObject({
status: 'failed',
retryable: false,
lastError: 'Bridge result manifest high watermark is stale',
});
await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
});
function createService(): OpenCodeStateChangingBridgeCommandService {
return new OpenCodeStateChangingBridgeCommandService({
expectedClientIdentity: clientIdentity,
handshakePort,
leaseStore,
ledger,
bridge,
manifestReader,
diagnostics,
requestIdFactory: () => 'cmd-1',
diagnosticIdFactory: () => 'diag-1',
clock: () => now,
});
}
});
function buildLaunchInput(): Parameters<OpenCodeStateChangingBridgeCommandService['execute']>[0] {
return {
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
capabilitySnapshotId: 'cap-1',
behaviorFingerprint: 'behavior-1',
body: { prompt: 'launch' },
cwd: '/tmp/project',
timeoutMs: 10_000,
};
}
function bridgeSuccess(
overrides: Partial<OpenCodeBridgeSuccess<unknown>> = {}
): OpenCodeBridgeSuccess<unknown> {
return {
ok: true,
schemaVersion: 1,
requestId: 'cmd-1',
command: 'opencode.launchTeam',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.0.0',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data: {
runId: 'run-1',
idempotencyKey: 'key-1',
runtimeStoreManifestHighWatermark: 10,
},
...overrides,
};
}
function peerIdentity(
peer: OpenCodeBridgePeerIdentity['peer'],
runtimeOverrides: Partial<OpenCodeBridgePeerIdentity['runtime']> = {}
): OpenCodeBridgePeerIdentity {
return {
schemaVersion: 1,
peer,
appVersion: '1.0.0',
gitSha: 'git-1',
buildId: 'build-1',
bridgeProtocol: {
minVersion: 1,
currentVersion: 1,
supportedCommands: [
'opencode.handshake',
'opencode.commandStatus',
'opencode.launchTeam',
'opencode.stopTeam',
],
},
runtime: {
providerId: 'opencode',
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.0.0',
capabilitySnapshotId: 'cap-1',
runtimeStoreManifestHighWatermark: 10,
activeRunId: 'run-1',
...runtimeOverrides,
},
featureFlags: {
opencodeTeamLaunch: true,
opencodeStateChangingCommands: true,
},
};
}
function buildHandshake(input: {
client: OpenCodeBridgePeerIdentity;
server: OpenCodeBridgePeerIdentity;
}): OpenCodeBridgeHandshake {
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
schemaVersion: 1,
requestId: 'handshake-1',
client: input.client,
server: input.server,
agreedProtocolVersion: 1,
acceptedCommands: ['opencode.launchTeam', 'opencode.stopTeam'],
serverTime: '2026-04-21T12:00:00.000Z',
};
return {
...withoutHash,
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
};
}
class FakeBridgeExecutor implements OpenCodeBridgeCommandExecutor {
calls: Array<{
command: OpenCodeBridgeCommandName;
body: { prompt: string; preconditions: { idempotencyKey: string } };
options: { cwd: string; timeoutMs: number; requestId?: string };
}> = [];
resultFactory: (input: {
command: OpenCodeBridgeCommandName;
body: { prompt: string; preconditions: { idempotencyKey: string } };
options: { cwd: string; timeoutMs: number; requestId?: string };
}) => OpenCodeBridgeResult<unknown> = ({ body, options }) =>
bridgeSuccess({
requestId: options.requestId,
data: {
runId: 'run-1',
idempotencyKey: body.preconditions.idempotencyKey,
runtimeStoreManifestHighWatermark: 10,
},
});
async execute<TBody, TData>(
command: OpenCodeBridgeCommandName,
body: TBody,
options: { cwd: string; timeoutMs: number; requestId?: string }
): Promise<OpenCodeBridgeResult<TData>> {
const call = {
command,
body: body as { prompt: string; preconditions: { idempotencyKey: string } },
options,
};
this.calls.push(call);
return this.resultFactory(call) as OpenCodeBridgeResult<TData>;
}
}
class FakeHandshakePort implements OpenCodeBridgeHandshakePort {
constructor(public nextHandshake: OpenCodeBridgeHandshake) {}
async handshake(): Promise<OpenCodeBridgeHandshake> {
return this.nextHandshake;
}
}
class FakeManifestReader implements RuntimeStoreManifestReader {
manifest: RuntimeStoreManifestEvidence = {
highWatermark: 10,
activeRunId: 'run-1',
capabilitySnapshotId: 'cap-1',
};
async read(): Promise<RuntimeStoreManifestEvidence> {
return this.manifest;
}
}
class FakeDiagnosticsSink implements OpenCodeStateChangingBridgeDiagnosticsSink {
readonly append = vi.fn(async () => {});
}

View file

@ -0,0 +1,219 @@
import { describe, expect, it, vi } from 'vitest';
import {
OpenCodeTaskLogAttributionService,
type OpenCodeTaskLogAttributionWriter,
} from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionService';
import type { OpenCodeTaskLogAttributionWriteResult } from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionStore';
function createWriter(result: OpenCodeTaskLogAttributionWriteResult = 'created') {
const writer: OpenCodeTaskLogAttributionWriter = {
upsertTaskRecord: vi.fn<OpenCodeTaskLogAttributionWriter['upsertTaskRecord']>(
async () => result
),
replaceTaskRecords: vi.fn<OpenCodeTaskLogAttributionWriter['replaceTaskRecords']>(
async () => result
),
clearTaskRecords: vi.fn<OpenCodeTaskLogAttributionWriter['clearTaskRecords']>(
async () => result
),
};
return writer;
}
describe('OpenCodeTaskLogAttributionService', () => {
it('records task-session attribution through the writer with launch defaults', async () => {
const writer = createWriter('created');
const now = new Date('2026-04-21T12:00:00.000Z');
const service = new OpenCodeTaskLogAttributionService(writer, () => now);
const outcome = await service.recordTaskSession({
teamName: ' team-a ',
taskId: ' task-a ',
memberName: ' alice ',
sessionId: ' session-a ',
since: new Date('2026-04-21T11:59:00.000Z'),
});
const expectedRecord = {
taskId: 'task-a',
memberName: 'alice',
scope: 'task_session' as const,
sessionId: 'session-a',
since: '2026-04-21T11:59:00.000Z',
source: 'launch_runtime' as const,
};
expect(outcome).toEqual({ result: 'created', record: expectedRecord });
expect(writer.upsertTaskRecord).toHaveBeenCalledWith('team-a', expectedRecord, { now });
expect(writer.replaceTaskRecords).not.toHaveBeenCalled();
});
it('rejects task-session attribution without a session id before writing', async () => {
const writer = createWriter();
const service = new OpenCodeTaskLogAttributionService(writer);
await expect(
service.recordTaskSession({
teamName: 'team-a',
taskId: 'task-a',
memberName: 'alice',
sessionId: ' ',
})
).rejects.toThrow('task_session requires sessionId');
expect(writer.upsertTaskRecord).not.toHaveBeenCalled();
});
it('rejects unsafe team, task, and member identifiers before writing', async () => {
const writer = createWriter();
const service = new OpenCodeTaskLogAttributionService(writer);
await expect(
service.recordTaskSession({
teamName: '../team-a',
taskId: 'task-a',
memberName: 'alice',
sessionId: 'session-a',
})
).rejects.toThrow('teamName contains invalid characters');
await expect(
service.recordTaskSession({
teamName: 'team-a',
taskId: '../task-a',
memberName: 'alice',
sessionId: 'session-a',
})
).rejects.toThrow('taskId contains invalid characters');
await expect(
service.recordTaskSession({
teamName: 'team-a',
taskId: 'task-a',
memberName: '../alice',
sessionId: 'session-a',
})
).rejects.toThrow('memberName contains invalid characters');
expect(writer.upsertTaskRecord).not.toHaveBeenCalled();
});
it('rejects broad or invalid member-window attribution before writing', async () => {
const writer = createWriter();
const service = new OpenCodeTaskLogAttributionService(writer);
await expect(
service.recordMemberSessionWindow({
teamName: 'team-a',
taskId: 'task-a',
memberName: 'alice',
sessionId: 'session-a',
})
).rejects.toThrow('requires since or startMessageUuid');
await expect(
service.recordMemberSessionWindow({
teamName: 'team-a',
taskId: 'task-a',
memberName: 'alice',
until: '2026-04-21T12:00:00.000Z',
})
).rejects.toThrow('requires since or startMessageUuid');
await expect(
service.recordMemberSessionWindow({
teamName: 'team-a',
taskId: 'task-a',
memberName: 'alice',
since: '2026-04-21T13:00:00.000Z',
until: '2026-04-21T12:00:00.000Z',
})
).rejects.toThrow('since must be before or equal to until');
expect(writer.upsertTaskRecord).not.toHaveBeenCalled();
});
it('records bounded member-window attribution through the writer with reconcile defaults', async () => {
const writer = createWriter('updated');
const now = new Date('2026-04-21T12:00:00.000Z');
const service = new OpenCodeTaskLogAttributionService(writer, () => now);
const outcome = await service.recordMemberSessionWindow({
teamName: 'team-a',
taskId: 'task-a',
memberName: 'bob',
sessionId: 'session-b',
startMessageUuid: ' m-1 ',
endMessageUuid: ' m-3 ',
});
const expectedRecord = {
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window' as const,
sessionId: 'session-b',
startMessageUuid: 'm-1',
endMessageUuid: 'm-3',
source: 'reconcile' as const,
};
expect(outcome).toEqual({ result: 'updated', record: expectedRecord });
expect(writer.upsertTaskRecord).toHaveBeenCalledWith('team-a', expectedRecord, { now });
});
it('replaces task attribution as a validated runtime snapshot', async () => {
const writer = createWriter('updated');
const now = new Date('2026-04-21T12:00:00.000Z');
const service = new OpenCodeTaskLogAttributionService(writer, () => now);
const outcome = await service.replaceTaskAttribution({
teamName: 'team-a',
taskId: 'task-a',
source: 'reconcile',
records: [
{
memberName: 'alice',
scope: 'task_session',
sessionId: 'session-a',
source: 'launch_runtime',
},
{
memberName: 'bob',
since: '2026-04-21T11:00:00Z',
until: new Date('2026-04-21T11:30:00.000Z'),
},
],
});
expect(outcome).toEqual({ result: 'updated', recordCount: 2 });
expect(writer.replaceTaskRecords).toHaveBeenCalledWith(
'team-a',
'task-a',
[
{
taskId: 'task-a',
memberName: 'alice',
scope: 'task_session',
sessionId: 'session-a',
source: 'launch_runtime',
},
{
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window',
since: '2026-04-21T11:00:00.000Z',
until: '2026-04-21T11:30:00.000Z',
source: 'reconcile',
},
],
{ now }
);
});
it('clears task attribution through the writer only for one task', async () => {
const writer = createWriter('deleted');
const service = new OpenCodeTaskLogAttributionService(writer);
await expect(
service.clearTaskAttribution({ teamName: ' team-a ', taskId: ' task-a ' })
).resolves.toEqual({ result: 'deleted', recordCount: 0 });
expect(writer.clearTaskRecords).toHaveBeenCalledWith('team-a', 'task-a');
expect(writer.upsertTaskRecord).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,313 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => ({
teamsBasePath: '',
}));
vi.mock('@main/utils/pathDecoder', () => ({
getTeamsBasePath: () => hoisted.teamsBasePath,
}));
import {
OpenCodeTaskLogAttributionStore,
getOpenCodeTaskLogAttributionPath,
} from '../../../../src/main/services/team/taskLogs/stream/OpenCodeTaskLogAttributionStore';
describe('OpenCodeTaskLogAttributionStore', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-attribution-'));
hoisted.teamsBasePath = tempDir;
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('reads normalized task records from tasks map and flat records without duplicates', async () => {
const filePath = getOpenCodeTaskLogAttributionPath('team-a');
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(
filePath,
JSON.stringify(
{
schemaVersion: 1,
tasks: {
'task-a': [
{
memberName: ' bob ',
scope: 'member_session_window',
sessionId: ' session-bob ',
since: '2026-04-21T12:00:00Z',
until: '2026-04-21T12:10:00Z',
source: 'launch_runtime',
},
{
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-bob',
since: '2026-04-21T12:00:00.000Z',
until: '2026-04-21T12:10:00.000Z',
source: 'launch_runtime',
},
{
memberName: '',
since: '2026-04-21T12:00:00Z',
},
],
},
records: [
{
taskId: 'task-a',
memberName: 'carol',
scope: 'task_session',
sessionId: 'session-carol',
startMessageUuid: 'm-1',
endMessageUuid: 'm-3',
createdAt: '2026-04-21T11:59:00Z',
},
{
taskId: 'other-task',
memberName: 'dave',
},
{
taskId: 'task-a',
memberName: 'erin',
since: '2026-04-21T13:00:00Z',
until: '2026-04-21T12:00:00Z',
},
],
},
null,
2
),
'utf8'
);
const records = await new OpenCodeTaskLogAttributionStore().readTaskRecords('team-a', 'task-a');
expect(records).toEqual([
{
taskId: 'task-a',
memberName: 'carol',
scope: 'task_session',
sessionId: 'session-carol',
startMessageUuid: 'm-1',
endMessageUuid: 'm-3',
createdAt: '2026-04-21T11:59:00.000Z',
},
{
taskId: 'task-a',
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-bob',
since: '2026-04-21T12:00:00.000Z',
until: '2026-04-21T12:10:00.000Z',
source: 'launch_runtime',
},
]);
});
it('degrades to empty records for missing, invalid, or unsupported files', async () => {
const store = new OpenCodeTaskLogAttributionStore();
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toEqual([]);
const filePath = getOpenCodeTaskLogAttributionPath('team-a');
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, '{bad-json', 'utf8');
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toEqual([]);
expect(vi.mocked(console.warn)).toHaveBeenCalledWith(
'[OpenCodeTaskLogAttributionStore]',
expect.stringContaining('invalid OpenCode task-log attribution JSON')
);
vi.mocked(console.warn).mockClear();
await fs.writeFile(
filePath,
JSON.stringify({
schemaVersion: 2,
records: [{ taskId: 'task-a', memberName: 'alice' }],
}),
'utf8'
);
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toEqual([]);
});
it('upserts records atomically and does not rewrite unchanged attribution', async () => {
const store = new OpenCodeTaskLogAttributionStore();
const now = new Date('2026-04-21T12:00:00.000Z');
await expect(
store.upsertTaskRecord(
'team-a',
{
taskId: 'task-a',
memberName: ' alice ',
scope: 'member_session_window',
sessionId: ' session-a ',
since: '2026-04-21T11:59:00Z',
until: '2026-04-21T12:05:00Z',
source: 'launch_runtime',
},
{ now }
)
).resolves.toBe('created');
const filePath = getOpenCodeTaskLogAttributionPath('team-a');
const firstRaw = await fs.readFile(filePath, 'utf8');
await expect(
store.upsertTaskRecord(
'team-a',
{
taskId: 'task-a',
memberName: 'alice',
scope: 'member_session_window',
sessionId: 'session-a',
since: '2026-04-21T11:59:00.000Z',
until: '2026-04-21T12:05:00.000Z',
source: 'launch_runtime',
},
{ now: new Date('2026-04-21T12:10:00.000Z') }
)
).resolves.toBe('unchanged');
expect(await fs.readFile(filePath, 'utf8')).toBe(firstRaw);
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toEqual([
{
taskId: 'task-a',
memberName: 'alice',
scope: 'member_session_window',
sessionId: 'session-a',
since: '2026-04-21T11:59:00.000Z',
until: '2026-04-21T12:05:00.000Z',
source: 'launch_runtime',
createdAt: '2026-04-21T12:00:00.000Z',
updatedAt: '2026-04-21T12:00:00.000Z',
},
]);
});
it('serializes concurrent upserts without losing records', async () => {
const store = new OpenCodeTaskLogAttributionStore();
await Promise.all([
store.upsertTaskRecord(
'team-a',
{
taskId: 'task-a',
memberName: 'alice',
scope: 'member_session_window',
sessionId: 'session-a',
since: '2026-04-21T10:00:00Z',
},
{ now: new Date('2026-04-21T10:00:01.000Z') }
),
store.upsertTaskRecord(
'team-a',
{
taskId: 'task-b',
memberName: 'bob',
scope: 'task_session',
sessionId: 'session-b',
startMessageUuid: 'm-1',
endMessageUuid: 'm-3',
},
{ now: new Date('2026-04-21T10:00:02.000Z') }
),
]);
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toMatchObject([
{
taskId: 'task-a',
memberName: 'alice',
sessionId: 'session-a',
},
]);
await expect(store.readTaskRecords('team-a', 'task-b')).resolves.toMatchObject([
{
taskId: 'task-b',
memberName: 'bob',
sessionId: 'session-b',
startMessageUuid: 'm-1',
endMessageUuid: 'm-3',
},
]);
});
it('replaces and clears only the requested task records', async () => {
const store = new OpenCodeTaskLogAttributionStore();
await store.upsertTaskRecord('team-a', {
taskId: 'task-a',
memberName: 'alice',
scope: 'member_session_window',
sessionId: 'session-a',
since: '2026-04-21T10:00:00Z',
});
await store.upsertTaskRecord('team-a', {
taskId: 'task-b',
memberName: 'bob',
scope: 'member_session_window',
sessionId: 'session-b',
since: '2026-04-21T11:00:00Z',
});
await expect(
store.replaceTaskRecords(
'team-a',
'task-a',
[
{
taskId: 'ignored-by-replace',
memberName: 'carol',
scope: 'task_session',
sessionId: 'session-c',
startMessageUuid: 'm-1',
},
],
{ now: new Date('2026-04-21T12:00:00.000Z') }
)
).resolves.toBe('updated');
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toMatchObject([
{
taskId: 'task-a',
memberName: 'carol',
sessionId: 'session-c',
startMessageUuid: 'm-1',
},
]);
await expect(store.readTaskRecords('team-a', 'task-b')).resolves.toMatchObject([
{
taskId: 'task-b',
memberName: 'bob',
sessionId: 'session-b',
},
]);
await expect(store.clearTaskRecords('team-a', 'task-a')).resolves.toBe('deleted');
await expect(store.readTaskRecords('team-a', 'task-a')).resolves.toEqual([]);
await expect(store.readTaskRecords('team-a', 'task-b')).resolves.toHaveLength(1);
});
it('fails closed instead of overwriting invalid attribution JSON during writes', async () => {
const store = new OpenCodeTaskLogAttributionStore();
const filePath = getOpenCodeTaskLogAttributionPath('team-a');
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, '{bad-json', 'utf8');
await expect(
store.upsertTaskRecord('team-a', {
taskId: 'task-a',
memberName: 'alice',
scope: 'member_session_window',
sessionId: 'session-a',
})
).rejects.toThrow('Invalid OpenCode task-log attribution JSON');
expect(await fs.readFile(filePath, 'utf8')).toBe('{bad-json');
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,432 @@
import { describe, expect, it, vi } from 'vitest';
import { createEmptyEndpointMap } from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
import {
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
import { REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import {
OpenCodeTeamLaunchReadinessService,
type OpenCodeApiCapabilityPort,
type OpenCodeModelExecutionProbePort,
type OpenCodeMcpToolProofPort,
type OpenCodeProductionE2EEvidencePort,
type OpenCodeRuntimeInventory,
type OpenCodeRuntimeInventoryPort,
type OpenCodeRuntimeStoreReadinessPort,
} from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import type {
OpenCodeApiCapabilities,
OpenCodeApiEndpointKey,
} from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
import type { OpenCodeMcpToolProof } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import type { RuntimeStoreReadinessCheck } from '../../../../src/main/services/team/opencode/store/RuntimeStoreManifest';
import type { OpenCodeProductionE2EEvidence } from '../../../../src/main/services/team/opencode/version/OpenCodeVersionPolicy';
describe('OpenCodeTeamLaunchReadinessService', () => {
it('returns not_installed before probing deeper runtime dependencies', async () => {
const ports = createPorts({
inventory: { detected: false, diagnostics: ['PATH checked'] },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'not_installed',
launchAllowed: false,
hostHealthy: false,
diagnostics: ['PATH checked', 'OpenCode CLI not detected on PATH'],
});
expect(ports.capabilities.detect).not.toHaveBeenCalled();
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('blocks unauthenticated OpenCode even when the binary is installed', async () => {
const ports = createPorts({
inventory: { authenticated: false, connectedProviders: [] },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
opencodeVersion: '1.14.19',
diagnostics: ['No connected OpenCode providers found'],
});
});
it('blocks unsupported versions before MCP and model probes', async () => {
const ports = createPorts({
inventory: { version: '1.4.0' },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'unsupported_version',
launchAllowed: false,
supportLevel: 'unsupported_too_old',
missing: ['OpenCode 1.4.0 is below supported minimum 1.14.19'],
});
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('blocks when API capabilities are missing required permission or tool routes', async () => {
const ports = createPorts({
capabilities: capabilities({ ready: false, missing: ['POST permission reply route'] }),
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'capabilities_missing',
launchAllowed: false,
permissionBridgeReady: false,
missing: ['POST permission reply route'],
evidence: {
capabilitiesReady: false,
},
});
});
it('blocks capability-compatible versions until production E2E evidence exists', async () => {
const ports = createPorts({
evidence: null,
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'e2e_missing',
launchAllowed: false,
supportLevel: 'supported_e2e_pending',
missing: ['OpenCode version is capability-compatible but production E2E evidence is missing'],
});
});
it('blocks when runtime stores need recovery before readiness', async () => {
const ports = createPorts({
runtimeStores: {
ok: false,
reason: 'runtime_store_recovery_required',
diagnostics: ['Incomplete batch must be reconciled before readiness'],
},
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'runtime_store_blocked',
launchAllowed: false,
runtimeStoresReady: false,
missing: ['Incomplete batch must be reconciled before readiness'],
evidence: {
runtimeStoreReadinessReason: 'runtime_store_recovery_required',
},
});
});
it('blocks when required app MCP tools are not proven through OpenCode', async () => {
const ports = createPorts({
toolProof: toolProof({
ok: false,
missingTools: ['runtime_deliver_message'],
diagnostics: [
'OpenCode missing canonical app MCP tool id agent-teams_runtime_deliver_message',
],
}),
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'mcp_unavailable',
launchAllowed: false,
appMcpConnected: true,
requiredToolsPresent: false,
missing: ['runtime_deliver_message'],
diagnostics: [
'OpenCode missing canonical app MCP tool id agent-teams_runtime_deliver_message',
],
});
});
it('runs optional execution probe and blocks unavailable selected model', async () => {
const ports = createPorts({
modelProbe: {
outcome: 'unavailable',
reason: 'model rejected by provider',
diagnostics: ['model rejected by provider'],
},
});
await expect(
service(ports).check(readinessInput({ requireExecutionProbe: true }))
).resolves.toMatchObject({
state: 'model_unavailable',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
missing: ['model rejected by provider'],
});
});
it('fails closed behind adapter feature gate after all runtime evidence is healthy', async () => {
const ports = createPorts();
await expect(
service(ports, { adapterEnabled: false }).check(readinessInput())
).resolves.toMatchObject({
state: 'adapter_disabled',
launchAllowed: false,
missing: ['OpenCode team launch adapter is disabled by feature gate'],
});
expect(ports.inventory.probe).not.toHaveBeenCalled();
});
it('allows dogfood launch to continue without production E2E evidence after runtime checks pass', async () => {
const ports = createPorts({ evidence: null });
await expect(
service(ports, { launchMode: 'dogfood' }).check(
readinessInput({ requireExecutionProbe: true })
)
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
supportLevel: 'supported_e2e_pending',
requiredToolsPresent: true,
runtimeStoresReady: true,
diagnostics: [
'OpenCode production E2E evidence is missing; dogfood launch remains allowed after runtime checks.',
],
});
expect(ports.mcpTools.prove).toHaveBeenCalled();
expect(ports.modelExecution.verify).toHaveBeenCalled();
});
it('allows launch only when inventory, capabilities, E2E, stores, MCP and model probe are healthy', async () => {
const ports = createPorts();
await expect(
service(ports, { adapterEnabled: true }).check(readinessInput())
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'openai/gpt-5.4-mini',
opencodeVersion: '1.14.19',
hostHealthy: true,
appMcpConnected: true,
requiredToolsPresent: true,
permissionBridgeReady: true,
runtimeStoresReady: true,
supportLevel: 'production_supported',
evidence: {
capabilitiesReady: true,
mcpToolProofRoute: '/experimental/tool/ids',
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
});
});
});
function service(
ports: ReturnType<typeof createPorts>,
options: { adapterEnabled?: boolean; launchMode?: 'disabled' | 'dogfood' | 'production' } = {}
): OpenCodeTeamLaunchReadinessService {
return new OpenCodeTeamLaunchReadinessService(
ports.inventory,
ports.capabilities,
ports.mcpTools,
ports.runtimeStores,
ports.modelExecution,
ports.e2eEvidence,
options.launchMode
? { launchMode: options.launchMode }
: { adapterEnabled: options.adapterEnabled ?? true }
);
}
function readinessInput(
overrides: Partial<{
projectPath: string;
selectedModel: string | null;
requireExecutionProbe: boolean;
}> = {}
) {
return {
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: false,
...overrides,
};
}
function createPorts(
overrides: {
inventory?: Partial<OpenCodeRuntimeInventory>;
capabilities?: OpenCodeApiCapabilities;
toolProof?: OpenCodeMcpToolProof;
runtimeStores?: RuntimeStoreReadinessCheck;
modelProbe?: {
outcome: 'available' | 'unavailable' | 'unknown';
reason: string | null;
diagnostics: string[];
};
evidence?: OpenCodeProductionE2EEvidence | null;
} = {}
): {
inventory: OpenCodeRuntimeInventoryPort & { probe: ReturnType<typeof vi.fn> };
capabilities: OpenCodeApiCapabilityPort & { detect: ReturnType<typeof vi.fn> };
mcpTools: OpenCodeMcpToolProofPort & { prove: ReturnType<typeof vi.fn> };
runtimeStores: OpenCodeRuntimeStoreReadinessPort & { check: ReturnType<typeof vi.fn> };
modelExecution: OpenCodeModelExecutionProbePort & { verify: ReturnType<typeof vi.fn> };
e2eEvidence: OpenCodeProductionE2EEvidencePort & { read: ReturnType<typeof vi.fn> };
} {
return {
inventory: {
probe: vi.fn(async () => inventory(overrides.inventory)),
},
capabilities: {
detect: vi.fn(async () => overrides.capabilities ?? capabilities()),
},
mcpTools: {
prove: vi.fn(async () => overrides.toolProof ?? toolProof()),
},
runtimeStores: {
check: vi.fn(async () => overrides.runtimeStores ?? runtimeStores()),
},
modelExecution: {
verify: vi.fn(async () => overrides.modelProbe ?? modelProbe()),
},
e2eEvidence: {
read: vi.fn(async () => (overrides.evidence === undefined ? evidence() : overrides.evidence)),
},
};
}
function inventory(overrides: Partial<OpenCodeRuntimeInventory> = {}): OpenCodeRuntimeInventory {
return {
detected: true,
binaryPath: '/opt/homebrew/bin/opencode',
installMethod: 'brew',
version: '1.14.19',
authenticated: true,
connectedProviders: ['openai'],
models: ['openai/gpt-5.4-mini'],
diagnostics: [],
...overrides,
};
}
function capabilities(
overrides: Partial<{
ready: boolean;
missing: string[];
}> = {}
): OpenCodeApiCapabilities {
const endpoints = createEmptyEndpointMap();
const evidence = {} as OpenCodeApiCapabilities['evidence'];
for (const key of Object.keys(endpoints) as OpenCodeApiEndpointKey[]) {
endpoints[key] = true;
evidence[key] = 'openapi';
}
if (overrides.ready === false) {
endpoints.permissionReply = false;
endpoints.permissionLegacySessionRespond = false;
}
return {
version: '1.14.19',
source: 'openapi_doc',
endpoints,
requiredForTeamLaunch: {
ready: overrides.ready ?? true,
missing: overrides.missing ?? [],
},
evidence,
diagnostics: [],
};
}
function toolProof(overrides: Partial<OpenCodeMcpToolProof> = {}): OpenCodeMcpToolProof {
return {
ok: true,
route: '/experimental/tool/ids',
canonicalServerName: 'agent_teams',
canonicalExpectedIds: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, `agent_teams_${tool}`])
),
observedTools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => `agent_teams_${tool}`),
missingTools: [],
matchedByRequiredTool: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, `agent_teams_${tool}`])
),
aliasMatchedByRequiredTool: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, null])
),
diagnostics: [],
...overrides,
};
}
function runtimeStores(): RuntimeStoreReadinessCheck {
return {
ok: true,
reason: 'runtime_store_manifest_valid',
diagnostics: [],
};
}
function modelProbe() {
return {
outcome: 'available' as const,
reason: null,
diagnostics: [],
};
}
function evidence(): OpenCodeProductionE2EEvidence {
const createdAt = new Date().toISOString();
const sessionId = 'session-1';
const requiredToolIds = REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => `agent_teams_${tool}`);
return {
schemaVersion: 1,
evidenceId: 'e2e-1',
createdAt,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
version: '1.14.19',
passed: true,
artifactPath: '/tmp/opencode-e2e',
binaryFingerprint: 'version:1.14.19',
capabilitySnapshotId: 'cap-1',
selectedModel: 'openai/gpt-5.4-mini',
projectPathFingerprint: 'project-a',
requiredSignals: Object.fromEntries(
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [signal, true])
) as OpenCodeProductionE2EEvidence['requiredSignals'],
mcpTools: {
requiredTools: requiredToolIds,
observedTools: requiredToolIds,
},
launch: {
runId: 'run-1',
teamId: 'team-a',
teamLaunchState: 'ready',
memberCount: 1,
sessions: [
{
memberName: 'Dev',
sessionId,
launchState: 'confirmed_alive',
},
],
durableCheckpoints: OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.map((name) => ({
name,
observedAt: createdAt,
})),
},
reconcile: {
runId: 'run-1',
teamLaunchState: 'ready',
memberCount: 1,
},
stop: {
runId: 'run-1',
stopped: true,
stoppedSessionIds: [sessionId],
},
logProjection: {
observed: true,
projectedMessageCount: 1,
},
};
}

View file

@ -0,0 +1,255 @@
import { describe, expect, it, vi } from 'vitest';
import {
OpenCodeTeamRuntimeAdapter,
type OpenCodeTeamRuntimeBridgePort,
type TeamRuntimeLaunchInput,
} from '../../../../src/main/services/team/runtime';
import type { OpenCodeTeamLaunchReadiness } from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import type { OpenCodeLaunchTeamCommandData } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import type { PersistedTeamLaunchSnapshot } from '../../../../src/shared/types';
describe('OpenCodeTeamRuntimeAdapter', () => {
it('maps readiness failures to a structured prepare block', async () => {
const bridge = bridgePort(
readiness({
state: 'mcp_unavailable',
launchAllowed: false,
missing: ['runtime_deliver_message'],
diagnostics: ['OpenCode missing canonical app MCP tool id'],
})
);
const adapter = new OpenCodeTeamRuntimeAdapter(bridge, { launchMode: 'production' });
await expect(adapter.prepare(launchInput())).resolves.toEqual({
ok: false,
providerId: 'opencode',
reason: 'mcp_unavailable',
retryable: true,
diagnostics: ['OpenCode missing canonical app MCP tool id', 'runtime_deliver_message'],
warnings: [],
});
expect(bridge.checkOpenCodeTeamLaunchReadiness).toHaveBeenCalledWith({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
launchMode: 'production',
});
});
it('fails closed when launch mode is disabled', async () => {
const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true }));
const adapter = new OpenCodeTeamRuntimeAdapter(
bridge
);
await expect(adapter.prepare(launchInput())).resolves.toMatchObject({
ok: false,
providerId: 'opencode',
reason: 'opencode_team_launch_disabled',
retryable: false,
});
expect(bridge.checkOpenCodeTeamLaunchReadiness).not.toHaveBeenCalled();
});
it('maps ready bridge launch data to successful runtime evidence only with required checkpoints', async () => {
const launchOpenCodeTeam = vi.fn(async () => ({
runId: 'run-1',
teamLaunchState: 'ready',
members: {
alice: {
sessionId: 'oc-session-1',
launchState: 'confirmed_alive',
model: 'openai/gpt-5.4-mini',
evidence: [
{ kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
],
},
},
warnings: [],
diagnostics: [],
}) satisfies OpenCodeLaunchTeamCommandData);
const adapter = new OpenCodeTeamRuntimeAdapter(
bridgePort(readiness({ state: 'ready', launchAllowed: true }), {
getLastOpenCodeRuntimeSnapshot: vi.fn(() => ({
providerId: 'opencode' as const,
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'version:1.14.19',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
})),
launchOpenCodeTeam,
}),
{ launchMode: 'dogfood' }
);
await expect(adapter.launch(launchInput())).resolves.toMatchObject({
runId: 'run-1',
teamName: 'team-a',
launchPhase: 'finished',
teamLaunchState: 'clean_success',
members: {
alice: {
providerId: 'opencode',
launchState: 'confirmed_alive',
sessionId: 'oc-session-1',
hardFailure: false,
},
},
});
expect(launchOpenCodeTeam).toHaveBeenCalledWith(
expect.objectContaining({
expectedCapabilitySnapshotId: 'cap-1',
manifestHighWatermark: null,
})
);
});
it('reconciles from existing persisted launch snapshot without treating OpenCode as truth', async () => {
const snapshot = launchSnapshot();
const adapter = new OpenCodeTeamRuntimeAdapter(
bridgePort(readiness({ state: 'adapter_disabled', launchAllowed: false }))
);
await expect(
adapter.reconcile({
runId: 'run-1',
teamName: 'team-a',
providerId: 'opencode',
expectedMembers: launchInput().expectedMembers,
previousLaunchState: snapshot,
reason: 'startup_recovery',
})
).resolves.toMatchObject({
runId: 'run-1',
teamName: 'team-a',
launchPhase: 'active',
teamLaunchState: 'partial_pending',
members: {
alice: {
providerId: 'opencode',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: true,
bootstrapConfirmed: false,
},
},
snapshot,
});
});
it('acknowledges stop without mutating live OpenCode ownership in the adapter shell', async () => {
const adapter = new OpenCodeTeamRuntimeAdapter(
bridgePort(readiness({ state: 'adapter_disabled', launchAllowed: false }))
);
await expect(
adapter.stop({
runId: 'run-1',
teamName: 'team-a',
providerId: 'opencode',
reason: 'user_requested',
previousLaunchState: launchSnapshot(),
})
).resolves.toMatchObject({
stopped: true,
members: {
alice: {
providerId: 'opencode',
stopped: true,
},
},
});
});
});
function bridgePort(
readinessResult: OpenCodeTeamLaunchReadiness,
overrides: Partial<OpenCodeTeamRuntimeBridgePort> = {}
): OpenCodeTeamRuntimeBridgePort {
return {
checkOpenCodeTeamLaunchReadiness: vi.fn(async () => readinessResult),
...overrides,
};
}
function launchInput(overrides: Partial<TeamRuntimeLaunchInput> = {}): TeamRuntimeLaunchInput {
return {
runId: 'run-1',
teamName: 'team-a',
cwd: '/repo',
providerId: 'opencode',
model: 'openai/gpt-5.4-mini',
skipPermissions: false,
expectedMembers: [
{
name: 'alice',
providerId: 'opencode',
model: 'openai/gpt-5.4-mini',
cwd: '/repo',
},
],
previousLaunchState: null,
...overrides,
};
}
function readiness(
overrides: Partial<OpenCodeTeamLaunchReadiness> = {}
): OpenCodeTeamLaunchReadiness {
return {
state: 'adapter_disabled',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
opencodeVersion: '1.14.19',
installMethod: 'brew',
binaryPath: '/opt/homebrew/bin/opencode',
hostHealthy: true,
appMcpConnected: true,
requiredToolsPresent: true,
permissionBridgeReady: true,
runtimeStoresReady: true,
supportLevel: 'production_supported',
missing: [],
diagnostics: [],
evidence: {
capabilitiesReady: true,
mcpToolProofRoute: '/experimental/tool/ids',
observedMcpTools: ['agent-teams_runtime_deliver_message'],
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
...overrides,
};
}
function launchSnapshot(): PersistedTeamLaunchSnapshot {
return {
version: 2,
teamName: 'team-a',
updatedAt: '2026-04-21T00:00:00.000Z',
launchPhase: 'active',
expectedMembers: ['alice'],
teamLaunchState: 'partial_pending',
summary: {
confirmedCount: 0,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
members: {
alice: {
name: 'alice',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
lastEvaluatedAt: '2026-04-21T00:00:00.000Z',
diagnostics: ['waiting for teammate check-in'],
},
},
};
}

Some files were not shown because too many files have changed in this diff Show more