From 2c3ccc60cfe4047b1a553e0f0d6289c86654ee93 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Fri, 5 Jun 2026 23:32:52 +0200 Subject: [PATCH 1/8] fix(runtime): wire relay mcp into cloud codex personas --- packages/runtime/src/cloud-defaults.ts | 181 ++++++++++++++++++++++++- packages/runtime/src/runner.test.ts | 147 ++++++++++++++++++++ 2 files changed, 325 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/cloud-defaults.ts b/packages/runtime/src/cloud-defaults.ts index ce1bee88..571d8e94 100644 --- a/packages/runtime/src/cloud-defaults.ts +++ b/packages/runtime/src/cloud-defaults.ts @@ -9,7 +9,8 @@ import { resolveMcpServersLenient, resolvePersonaInputs, resolveStringMapLenient, - type PersonaSpec + type PersonaSpec, + type RelayMcpConfig } from '@agentworkforce/persona-kit'; import { createDefaultLlm } from './cloud-llm.js'; import { SandboxNotAvailableError } from './errors.js'; @@ -459,7 +460,8 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { log: args.log }); const task = run.prompt; - const spec = buildNonInteractiveSpec({ + const relayMcp = resolveRelayMcpFromEnv(args.env); + const specInput = { harness, personaId: args.persona.id, model: personaModel, @@ -470,7 +472,31 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { task, name: args.persona.id, workingDirectory: cwd + }; + let spec = buildNonInteractiveSpec({ + ...specInput, + ...(relayMcp && harness !== 'codex' ? { relayMcp } : {}) }); + let spawnArgs = [...spec.args]; + if (relayMcp && harness === 'codex') { + const brokerMcpArgs = await resolveCodexAgentRelayMcpArgs({ + env: args.env, + relayMcp, + cwd, + existingArgs: codexExistingArgs(spawnArgs), + log: args.log + }); + if (brokerMcpArgs) { + spawnArgs = injectCodexSubcommandArgs(spawnArgs, brokerMcpArgs); + } else { + // Legacy compatibility fallback. The broker's `agent-relay` MCP server + // is preferred for Codex because `mcp-args --register` pre-mints + // RELAY_AGENT_TOKEN and sets RELAY_SKIP_BOOTSTRAP=1; the older + // `@relaycast/mcp` server self-registers during MCP initialize. + spec = buildNonInteractiveSpec({ ...specInput, relayMcp }); + spawnArgs = [...spec.args]; + } + } for (const warning of spec.warnings) { args.log('warn', 'harness.spec.warning', { warning }); } @@ -491,7 +517,7 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { }; const result = await spawnAndCapture({ bin: spec.bin, - args: [...spec.args], + args: spawnArgs, cwd, env: childEnv, timeoutMs: args.persona.harnessSettings.timeoutSeconds @@ -518,6 +544,155 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { }; } +interface BrokerMcpArgsOutput { + args: string[]; + sideEffectFiles?: string[]; + agentToken?: string | null; +} + +function resolveRelayMcpFromEnv(env: NodeJS.ProcessEnv): RelayMcpConfig | undefined { + const apiKey = env.RELAY_API_KEY?.trim(); + const agentName = env.RELAY_AGENT_NAME?.trim(); + if (!apiKey || !agentName) return undefined; + const baseUrl = env.RELAY_BASE_URL?.trim(); + const defaultWorkspace = env.RELAY_DEFAULT_WORKSPACE?.trim(); + return { + apiKey, + agentName, + ...(baseUrl ? { baseUrl } : {}), + ...(defaultWorkspace ? { defaultWorkspace } : {}) + }; +} + +async function resolveCodexAgentRelayMcpArgs(args: { + env: NodeJS.ProcessEnv; + relayMcp: RelayMcpConfig; + cwd: string; + existingArgs: string[]; + log: WorkforceCtx['log']; +}): Promise { + const broker = resolveAgentRelayBrokerBinary(args.env); + const baseUrl = args.relayMcp.baseUrl ?? 'https://api.relaycast.dev'; + const brokerArgs = [ + 'mcp-args', + '--cli', + 'codex', + '--agent-name', + args.relayMcp.agentName, + '--api-key', + args.relayMcp.apiKey, + '--base-url', + baseUrl, + '--register', + '--cwd', + args.cwd, + '--existing-args', + JSON.stringify(args.existingArgs) + ]; + const workspacesJson = args.env.RELAY_WORKSPACES_JSON?.trim(); + if (workspacesJson) brokerArgs.push('--workspaces-json', workspacesJson); + if (args.relayMcp.defaultWorkspace) { + brokerArgs.push('--default-workspace', args.relayMcp.defaultWorkspace); + } + + const result = await spawnAndCapture({ + bin: broker, + args: brokerArgs, + cwd: args.cwd, + env: args.env, + timeoutMs: 15_000 + }); + if (result.exitCode !== 0) { + args.log('warn', 'harness.relay_mcp.broker_args_failed', { + broker, + exitCode: result.exitCode, + stderr: result.stderr.trim() + }); + return undefined; + } + + let parsed: unknown; + try { + parsed = JSON.parse(result.output); + } catch (err) { + args.log('warn', 'harness.relay_mcp.broker_args_invalid_json', { + broker, + error: err instanceof Error ? err.message : String(err) + }); + return undefined; + } + if (!isBrokerMcpArgsOutput(parsed)) { + args.log('warn', 'harness.relay_mcp.broker_args_invalid_shape', { broker }); + return undefined; + } + if (parsed.sideEffectFiles?.length) { + args.log('debug', 'harness.relay_mcp.side_effect_files', { + files: parsed.sideEffectFiles + }); + } + return parsed.args; +} + +function resolveAgentRelayBrokerBinary(env: NodeJS.ProcessEnv): string { + const configured = env.AGENT_RELAY_BIN?.trim() || env.BROKER_BINARY_PATH?.trim(); + if (configured) return configured; + const sandboxBroker = resolveSandboxAgentRelayBrokerBinary(); + return sandboxBroker ?? 'agent-relay-broker'; +} + +function resolveSandboxAgentRelayBrokerBinary(): string | undefined { + const suffix = agentRelayBrokerPlatformSuffix(); + if (!suffix) return undefined; + const candidate = path.join( + '/opt/relay-smoke/node_modules/@agent-relay/sdk/bin', + `agent-relay-broker-${suffix}` + ); + return canExecuteFileSync(candidate) ? candidate : undefined; +} + +function agentRelayBrokerPlatformSuffix(): string | undefined { + const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : undefined; + if (!arch) return undefined; + if (process.platform === 'linux') return `linux-${arch}`; + if (process.platform === 'darwin') return `darwin-${arch}`; + if (process.platform === 'win32') return 'win32-x64.exe'; + return undefined; +} + +function canExecuteFileSync(candidate: string): boolean { + try { + accessSync(candidate, constants.R_OK | constants.X_OK); + return statSync(candidate).isFile(); + } catch { + return false; + } +} + +function codexExistingArgs(args: string[]): string[] { + return args[0] === 'exec' ? args.slice(1) : [...args]; +} + +function injectCodexSubcommandArgs(args: string[], injected: string[]): string[] { + if (args[0] === 'exec') return ['exec', ...injected, ...args.slice(1)]; + if (args.length === 0 || args[0]?.startsWith('-')) return [...injected, ...args]; + return [...args]; +} + +function isBrokerMcpArgsOutput(value: unknown): value is BrokerMcpArgsOutput { + if (!isRecord(value) || !Array.isArray(value.args)) return false; + if (!value.args.every((arg) => typeof arg === 'string')) return false; + if ( + value.sideEffectFiles !== undefined && + (!Array.isArray(value.sideEffectFiles) || + !value.sideEffectFiles.every((file) => typeof file === 'string')) + ) { + return false; + } + return value.agentToken === undefined || + value.agentToken === null || + typeof value.agentToken === 'string'; +} + async function materializeSidecar(args: { persona: PersonaSpec; inputValues: Record; diff --git a/packages/runtime/src/runner.test.ts b/packages/runtime/src/runner.test.ts index ffeafc33..fc6f6514 100644 --- a/packages/runtime/src/runner.test.ts +++ b/packages/runtime/src/runner.test.ts @@ -204,6 +204,153 @@ async function writeFakeHarness(binDir: string, name: string, stdout: string): P await chmod(path.join(binDir, name), 0o755); } +async function writeArgCaptureHarness( + binDir: string, + name: string, + capturePath: string +): Promise { + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(binDir, name), + [ + '#!/usr/bin/env node', + "const fs = require('node:fs');", + `fs.writeFileSync(${JSON.stringify(capturePath)}, JSON.stringify({ argv: process.argv.slice(2) }, null, 2));`, + "process.stdout.write('ok\\n');" + ].join('\n'), + 'utf8' + ); + await chmod(path.join(binDir, name), 0o755); +} + +async function writeFakeBroker(binDir: string, capturePath: string): Promise { + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(binDir, 'agent-relay-broker'), + [ + '#!/usr/bin/env node', + "const fs = require('node:fs');", + 'const argv = process.argv.slice(2);', + `fs.writeFileSync(${JSON.stringify(capturePath)}, JSON.stringify({ argv }, null, 2));`, + 'process.stdout.write(JSON.stringify({', + ' args: [', + ' "--config",', + ' "check_for_update_on_startup=false",', + ' "--config",', + ' "mcp_servers.agent-relay.command=\\"npx\\"",', + ' "--config",', + ' "mcp_servers.agent-relay.args=[\\"-y\\", \\"agent-relay\\", \\"mcp\\"]",', + ' "--config",', + ' "mcp_servers.agent-relay.env.RELAY_AGENT_TOKEN=\\"at_live_test\\"",', + ' "--config",', + ' "mcp_servers.agent-relay.env.RELAY_SKIP_BOOTSTRAP=\\"1\\""', + ' ],', + ' sideEffectFiles: [],', + ' agentToken: "at_live_test"', + '}));' + ].join('\n'), + 'utf8' + ); + await chmod(path.join(binDir, 'agent-relay-broker'), 0o755); +} + +test('cloud default codex harness injects agent-relay MCP args from broker helper', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'codex-argv.json'); + const brokerCapturePath = path.join(tempDir, 'broker-argv.json'); + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'codex', capturePath); + await writeFakeBroker(binDir, brokerCapturePath); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + id: 'autonomous-actor', + harness: 'codex', + model: 'openai/gpt-5', + systemPrompt: 'coordinate with the team', + harnessSettings: { + reasoning: 'medium', + dangerouslyBypassApprovalsAndSandbox: true, + timeoutSeconds: 5 + } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: () => {}, + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'codex-1', + RELAY_BASE_URL: 'https://relay.example.test', + RELAY_DEFAULT_WORKSPACE: 'ws-relay', + RELAY_WORKSPACES_JSON: '{"workspaces":[{"id":"ws-relay"}]}' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + + const captured = JSON.parse(await readFile(capturePath, 'utf8')) as { argv: string[] }; + assert.deepEqual(captured.argv.slice(0, 9), [ + 'exec', + '--config', + 'check_for_update_on_startup=false', + '--config', + 'mcp_servers.agent-relay.command="npx"', + '--config', + 'mcp_servers.agent-relay.args=["-y", "agent-relay", "mcp"]', + '--config', + 'mcp_servers.agent-relay.env.RELAY_AGENT_TOKEN="at_live_test"' + ]); + assert.equal(captured.argv[9], '--config'); + assert.equal(captured.argv[10], 'mcp_servers.agent-relay.env.RELAY_SKIP_BOOTSTRAP="1"'); + assert.equal(captured.argv[11], '-m'); + assert.equal(captured.argv[12], 'gpt-5'); + assert.ok(captured.argv.includes('--dangerously-bypass-approvals-and-sandbox')); + assert.ok(captured.argv.includes('--skip-git-repo-check')); + + const brokerCaptured = JSON.parse(await readFile(brokerCapturePath, 'utf8')) as { + argv: string[]; + }; + assert.deepEqual(brokerCaptured.argv.slice(0, 11), [ + 'mcp-args', + '--cli', + 'codex', + '--agent-name', + 'codex-1', + '--api-key', + 'rk_live_test', + '--base-url', + 'https://relay.example.test', + '--register', + '--cwd' + ]); + assert.equal(brokerCaptured.argv[11], workspaceRoot); + const existingArgsIdx = brokerCaptured.argv.indexOf('--existing-args'); + assert.notEqual(existingArgsIdx, -1); + assert.deepEqual( + JSON.parse(brokerCaptured.argv[existingArgsIdx + 1]), + captured.argv.slice(11) + ); + assert.equal( + brokerCaptured.argv[brokerCaptured.argv.indexOf('--workspaces-json') + 1], + '{"workspaces":[{"id":"ws-relay"}]}' + ); + assert.equal( + brokerCaptured.argv[brokerCaptured.argv.indexOf('--default-workspace') + 1], + 'ws-relay' + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + function snapshotEnv(keys: string[]): Record { const out: Record = {}; for (const key of keys) out[key] = process.env[key]; From 4b4b887a54dbbcc6ce57f20c6deb3791ea373d6c Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Fri, 5 Jun 2026 21:44:15 +0000 Subject: [PATCH 2/8] chore: apply pr-reviewer fixes for #205 --- memory/workspace/.relay/state.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 memory/workspace/.relay/state.json diff --git a/memory/workspace/.relay/state.json b/memory/workspace/.relay/state.json new file mode 100644 index 00000000..1a2bcf77 --- /dev/null +++ b/memory/workspace/.relay/state.json @@ -0,0 +1 @@ +{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:44:11.894532217Z","lastSuccessfulReconcileAt":"2026-06-05T21:44:11.894532217Z","staleAfter":"2026-06-05T21:44:21.894532217Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":6},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file From 6ed26438751250d8edd7c84c5f22cae70df6d001 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Fri, 5 Jun 2026 23:46:05 +0200 Subject: [PATCH 3/8] fix(runtime): wire relay mcp into cloud personas --- packages/runtime/src/cloud-defaults.ts | 113 ++++++++++++- packages/runtime/src/runner.test.ts | 209 +++++++++++++++++++++++-- 2 files changed, 306 insertions(+), 16 deletions(-) diff --git a/packages/runtime/src/cloud-defaults.ts b/packages/runtime/src/cloud-defaults.ts index 571d8e94..22ed0847 100644 --- a/packages/runtime/src/cloud-defaults.ts +++ b/packages/runtime/src/cloud-defaults.ts @@ -473,13 +473,15 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { name: args.persona.id, workingDirectory: cwd }; + const brokerRelayHarness = relayMcp && (harness === 'claude' || harness === 'codex'); let spec = buildNonInteractiveSpec({ ...specInput, - ...(relayMcp && harness !== 'codex' ? { relayMcp } : {}) + ...(relayMcp && !brokerRelayHarness ? { relayMcp } : {}) }); let spawnArgs = [...spec.args]; if (relayMcp && harness === 'codex') { - const brokerMcpArgs = await resolveCodexAgentRelayMcpArgs({ + const brokerMcpArgs = await resolveAgentRelayBrokerMcpArgs({ + cli: 'codex', env: args.env, relayMcp, cwd, @@ -496,6 +498,39 @@ function createProcessHarnessRunner(args: CloudDefaultOptions & { spec = buildNonInteractiveSpec({ ...specInput, relayMcp }); spawnArgs = [...spec.args]; } + } else if (relayMcp && harness === 'claude') { + if (claudeMcpConfigHasRelayOverride(spawnArgs)) { + args.log('debug', 'harness.relay_mcp.persona_override', { + harness, + serverNames: relayOverrideServerNames(spawnArgs) + }); + } else { + const brokerMcpArgs = await resolveAgentRelayBrokerMcpArgs({ + cli: 'claude', + env: args.env, + relayMcp, + cwd, + // Claude persona specs already contain --mcp-config; the broker + // treats that as user-managed MCP and returns no injection args. + // Ask for the canonical broker payload, then merge agent-relay into + // the persona's strict config below. + existingArgs: [], + log: args.log + }); + const mergedArgs = brokerMcpArgs + ? injectClaudeAgentRelayMcpConfig(spawnArgs, brokerMcpArgs, args.log) + : undefined; + if (mergedArgs) { + spawnArgs = mergedArgs; + } else { + // Legacy compatibility fallback. The broker-generated `agent-relay` + // MCP server is preferred because it comes from the Relay SDK broker + // helper and carries the pre-registered token fast path; the older + // `@relaycast/mcp` server self-registers during MCP initialize. + spec = buildNonInteractiveSpec({ ...specInput, relayMcp }); + spawnArgs = [...spec.args]; + } + } } for (const warning of spec.warnings) { args.log('warn', 'harness.spec.warning', { warning }); @@ -564,7 +599,8 @@ function resolveRelayMcpFromEnv(env: NodeJS.ProcessEnv): RelayMcpConfig | undefi }; } -async function resolveCodexAgentRelayMcpArgs(args: { +async function resolveAgentRelayBrokerMcpArgs(args: { + cli: 'claude' | 'codex'; env: NodeJS.ProcessEnv; relayMcp: RelayMcpConfig; cwd: string; @@ -576,7 +612,7 @@ async function resolveCodexAgentRelayMcpArgs(args: { const brokerArgs = [ 'mcp-args', '--cli', - 'codex', + args.cli, '--agent-name', args.relayMcp.agentName, '--api-key', @@ -643,6 +679,9 @@ function resolveAgentRelayBrokerBinary(env: NodeJS.ProcessEnv): string { function resolveSandboxAgentRelayBrokerBinary(): string | undefined { const suffix = agentRelayBrokerPlatformSuffix(); if (!suffix) return undefined; + // Daytona cloud images install the Relay SDK smoke dependency here. This is + // a compatibility fallback; env overrides and PATH remain the general SDK + // contract for locating agent-relay-broker. const candidate = path.join( '/opt/relay-smoke/node_modules/@agent-relay/sdk/bin', `agent-relay-broker-${suffix}` @@ -678,6 +717,72 @@ function injectCodexSubcommandArgs(args: string[], injected: string[]): string[] return [...args]; } +function injectClaudeAgentRelayMcpConfig( + args: string[], + injected: string[], + log: WorkforceCtx['log'] +): string[] | undefined { + const base = parseClaudeMcpConfigArg(args); + const broker = parseClaudeMcpConfigArg(injected); + if (!base || !broker) { + log('warn', 'harness.relay_mcp.claude_mcp_config_missing'); + return undefined; + } + const baseServers = readMcpServersRecord(base.payload); + const brokerServers = readMcpServersRecord(broker.payload); + const agentRelay = brokerServers?.['agent-relay']; + if (!baseServers || !brokerServers || agentRelay === undefined) { + log('warn', 'harness.relay_mcp.claude_mcp_config_invalid'); + return undefined; + } + const mergedServers: Record = { + ...baseServers, + 'agent-relay': agentRelay + }; + delete mergedServers.relaycast; + const nextPayload = { + ...base.payload, + mcpServers: mergedServers + }; + const next = [...args]; + next[base.valueIndex] = JSON.stringify(nextPayload); + return next; +} + +function claudeMcpConfigHasRelayOverride(args: string[]): boolean { + return relayOverrideServerNames(args).length > 0; +} + +function relayOverrideServerNames(args: string[]): string[] { + const parsed = parseClaudeMcpConfigArg(args); + const servers = parsed ? readMcpServersRecord(parsed.payload) : undefined; + if (!servers) return []; + return ['agent-relay', 'relaycast'].filter((name) => servers[name] !== undefined); +} + +function parseClaudeMcpConfigArg( + args: string[] +): { valueIndex: number; payload: Record } | undefined { + const flagIndex = args.indexOf('--mcp-config'); + if (flagIndex < 0) return undefined; + const valueIndex = flagIndex + 1; + const raw = args[valueIndex]; + if (typeof raw !== 'string') return undefined; + try { + const payload = JSON.parse(raw); + return isRecord(payload) ? { valueIndex, payload } : undefined; + } catch { + return undefined; + } +} + +function readMcpServersRecord( + payload: Record +): Record | undefined { + const servers = payload.mcpServers; + return isRecord(servers) ? servers : undefined; +} + function isBrokerMcpArgsOutput(value: unknown): value is BrokerMcpArgsOutput { if (!isRecord(value) || !Array.isArray(value.args)) return false; if (!value.args.every((arg) => typeof arg === 'string')) return false; diff --git a/packages/runtime/src/runner.test.ts b/packages/runtime/src/runner.test.ts index fc6f6514..4e527cd4 100644 --- a/packages/runtime/src/runner.test.ts +++ b/packages/runtime/src/runner.test.ts @@ -231,20 +231,42 @@ async function writeFakeBroker(binDir: string, capturePath: string): Promise { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'claude-argv.json'); + const brokerCapturePath = path.join(tempDir, 'broker-argv.json'); + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'claude', capturePath); + await writeFakeBroker(binDir, brokerCapturePath); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + id: 'autonomous-actor', + harness: 'claude', + model: 'anthropic/claude-3-5-sonnet', + systemPrompt: 'coordinate with the team', + harnessSettings: { reasoning: 'medium', timeoutSeconds: 5 }, + mcpServers: { + filesystem: { type: 'stdio', command: 'filesystem-mcp' } + } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: () => {}, + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'claude-1', + RELAY_BASE_URL: 'https://relay.example.test', + RELAY_DEFAULT_WORKSPACE: 'ws-relay', + RELAY_WORKSPACES_JSON: '{"workspaces":[{"id":"ws-relay"}]}' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + + const captured = JSON.parse(await readFile(capturePath, 'utf8')) as { argv: string[] }; + assert.equal(captured.argv.filter((arg) => arg === '--mcp-config').length, 1); + assert.ok(captured.argv.includes('--strict-mcp-config')); + assert.ok(captured.argv.includes('--print')); + assert.ok(captured.argv.includes('--output-format')); + assert.equal(captured.argv.at(-1), 'do the work'); + + const mcpConfigIdx = captured.argv.indexOf('--mcp-config'); + const payload = JSON.parse(captured.argv[mcpConfigIdx + 1]) as { + mcpServers: Record< + string, + { type: string; command?: string; args?: string[]; env?: Record } + >; + }; + assert.deepEqual(payload.mcpServers.filesystem, { + type: 'stdio', + command: 'filesystem-mcp' + }); + assert.equal(payload.mcpServers.relaycast, undefined); + assert.equal(payload.mcpServers['agent-relay'].command, 'npx'); + assert.deepEqual(payload.mcpServers['agent-relay'].args, ['-y', 'agent-relay', 'mcp']); + assert.equal(payload.mcpServers['agent-relay'].env?.RELAY_AGENT_TOKEN, 'at_live_test'); + assert.equal(payload.mcpServers['agent-relay'].env?.RELAY_SKIP_BOOTSTRAP, '1'); + assert.equal(payload.mcpServers['agent-relay'].env?.RELAY_STRICT_AGENT_NAME, '1'); + + const brokerCaptured = JSON.parse(await readFile(brokerCapturePath, 'utf8')) as { + argv: string[]; + }; + assert.deepEqual(brokerCaptured.argv.slice(0, 11), [ + 'mcp-args', + '--cli', + 'claude', + '--agent-name', + 'claude-1', + '--api-key', + 'rk_live_test', + '--base-url', + 'https://relay.example.test', + '--register', + '--cwd' + ]); + assert.equal(brokerCaptured.argv[11], workspaceRoot); + const existingArgsIdx = brokerCaptured.argv.indexOf('--existing-args'); + assert.notEqual(existingArgsIdx, -1); + assert.deepEqual(JSON.parse(brokerCaptured.argv[existingArgsIdx + 1]), []); + assert.equal( + brokerCaptured.argv[brokerCaptured.argv.indexOf('--workspaces-json') + 1], + '{"workspaces":[{"id":"ws-relay"}]}' + ); + assert.equal( + brokerCaptured.argv[brokerCaptured.argv.indexOf('--default-workspace') + 1], + 'ws-relay' + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('cloud default claude harness preserves explicit relay MCP overrides', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'claude-argv.json'); + const brokerCapturePath = path.join(tempDir, 'broker-argv.json'); + const logs: Array<{ level: string; message: string; data?: unknown }> = []; + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'claude', capturePath); + await writeFakeBroker(binDir, brokerCapturePath); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + id: 'autonomous-actor', + harness: 'claude', + model: 'anthropic/claude-3-5-sonnet', + systemPrompt: 'coordinate with the team', + harnessSettings: { reasoning: 'medium', timeoutSeconds: 5 }, + mcpServers: { + relaycast: { type: 'stdio', command: 'custom-relaycast' } + } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: (level, message, data) => logs.push({ level, message, data }), + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'claude-1', + RELAY_BASE_URL: 'https://relay.example.test' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + + const captured = JSON.parse(await readFile(capturePath, 'utf8')) as { argv: string[] }; + const mcpConfigIdx = captured.argv.indexOf('--mcp-config'); + const payload = JSON.parse(captured.argv[mcpConfigIdx + 1]) as { + mcpServers: Record; + }; + assert.deepEqual(payload.mcpServers.relaycast, { + type: 'stdio', + command: 'custom-relaycast' + }); + assert.equal(payload.mcpServers['agent-relay'], undefined); + await assert.rejects(readFile(brokerCapturePath, 'utf8'), /ENOENT/); + assert.deepEqual( + logs.find((log) => log.message === 'harness.relay_mcp.persona_override'), + { + level: 'debug', + message: 'harness.relay_mcp.persona_override', + data: { harness: 'claude', serverNames: ['relaycast'] } + } + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + function snapshotEnv(keys: string[]): Record { const out: Record = {}; for (const key of keys) out[key] = process.env[key]; From a5aa09c9455c4f7ea0e837ec1bfe75a13fe0b7d6 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Fri, 5 Jun 2026 21:46:47 +0000 Subject: [PATCH 4/8] chore: apply pr-reviewer fixes for #205 --- memory/workspace/.relay/state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory/workspace/.relay/state.json b/memory/workspace/.relay/state.json index 1a2bcf77..d0f6e719 100644 --- a/memory/workspace/.relay/state.json +++ b/memory/workspace/.relay/state.json @@ -1 +1 @@ -{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:44:11.894532217Z","lastSuccessfulReconcileAt":"2026-06-05T21:44:11.894532217Z","staleAfter":"2026-06-05T21:44:21.894532217Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":6},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file +{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:46:45.026651727Z","lastSuccessfulReconcileAt":"2026-06-05T21:46:45.026651727Z","staleAfter":"2026-06-05T21:46:55.026651727Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":16},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file From 0144dc8bc7935699fed74e843e171e378ea83e69 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Fri, 5 Jun 2026 21:48:30 +0000 Subject: [PATCH 5/8] chore: apply pr-reviewer fixes for #205 --- memory/workspace/.relay/state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory/workspace/.relay/state.json b/memory/workspace/.relay/state.json index d0f6e719..c27a3ac4 100644 --- a/memory/workspace/.relay/state.json +++ b/memory/workspace/.relay/state.json @@ -1 +1 @@ -{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:46:45.026651727Z","lastSuccessfulReconcileAt":"2026-06-05T21:46:45.026651727Z","staleAfter":"2026-06-05T21:46:55.026651727Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":16},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file +{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:48:28.648705871Z","lastSuccessfulReconcileAt":"2026-06-05T21:48:28.648705871Z","staleAfter":"2026-06-05T21:48:38.648705871Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":98},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file From b0875b8b631a7a5bb53e16f40a50f1f97a820705 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Fri, 5 Jun 2026 21:50:28 +0000 Subject: [PATCH 6/8] chore: apply pr-reviewer fixes for #205 --- memory/workspace/.relay/state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory/workspace/.relay/state.json b/memory/workspace/.relay/state.json index c27a3ac4..a44ac2f4 100644 --- a/memory/workspace/.relay/state.json +++ b/memory/workspace/.relay/state.json @@ -1 +1 @@ -{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:48:28.648705871Z","lastSuccessfulReconcileAt":"2026-06-05T21:48:28.648705871Z","staleAfter":"2026-06-05T21:48:38.648705871Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":98},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file +{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:50:26.129907795Z","lastSuccessfulReconcileAt":"2026-06-05T21:50:26.129907795Z","staleAfter":"2026-06-05T21:50:36.129907795Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":107},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file From 4812be79b4dd569e676d09e62c3819ca0e5ba113 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Fri, 5 Jun 2026 23:52:18 +0200 Subject: [PATCH 7/8] chore: ignore relay workspace state --- .gitignore | 2 ++ memory/workspace/.relay/state.json | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 memory/workspace/.relay/state.json diff --git a/.gitignore b/.gitignore index 2001269d..44935582 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ packages/*/dist/ .agent-relay # workforce deploy bundle output (staged per-persona by `workforce deploy`) **/.workforce/ +# RelayFile workspace sync sidecar state (regenerated per-tick in sandbox) +**/.relay/ diff --git a/memory/workspace/.relay/state.json b/memory/workspace/.relay/state.json deleted file mode 100644 index a44ac2f4..00000000 --- a/memory/workspace/.relay/state.json +++ /dev/null @@ -1 +0,0 @@ -{"workspaceId":"rw_7ccfea89","remoteRoot":"/memory/workspace","localRoot":"/home/daytona/workspace/memory/workspace","mode":"poll","intervalMs":5000,"lastReconcileAt":"2026-06-05T21:50:26.129907795Z","lastSuccessfulReconcileAt":"2026-06-05T21:50:26.129907795Z","staleAfter":"2026-06-05T21:50:36.129907795Z","status":"ready","states":{"stale":false,"offline":false,"hasConflicts":false,"hasPendingWriteback":false},"pendingWriteback":0,"pendingConflicts":0,"deniedPaths":0,"counters":{"snapshotDeleteBlocked":107},"circuit":{"open":false,"openedAt":"0001-01-01T00:00:00Z","windowMs":60000,"cooldownMs":30000,"threshold":5,"nextRetry":"0001-01-01T00:00:00Z"}} \ No newline at end of file From f717b1542609397ad259d5bcf6bbee4b6cfcb3ff Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Fri, 5 Jun 2026 22:19:13 +0000 Subject: [PATCH 8/8] chore: apply pr-reviewer fixes for #205 --- packages/runtime/src/cloud-defaults.ts | 18 ++- packages/runtime/src/runner.test.ts | 159 ++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/cloud-defaults.ts b/packages/runtime/src/cloud-defaults.ts index 22ed0847..8c4e513c 100644 --- a/packages/runtime/src/cloud-defaults.ts +++ b/packages/runtime/src/cloud-defaults.ts @@ -608,7 +608,6 @@ async function resolveAgentRelayBrokerMcpArgs(args: { log: WorkforceCtx['log']; }): Promise { const broker = resolveAgentRelayBrokerBinary(args.env); - const baseUrl = args.relayMcp.baseUrl ?? 'https://api.relaycast.dev'; const brokerArgs = [ 'mcp-args', '--cli', @@ -617,8 +616,7 @@ async function resolveAgentRelayBrokerMcpArgs(args: { args.relayMcp.agentName, '--api-key', args.relayMcp.apiKey, - '--base-url', - baseUrl, + ...(args.relayMcp.baseUrl ? ['--base-url', args.relayMcp.baseUrl] : []), '--register', '--cwd', args.cwd, @@ -642,7 +640,7 @@ async function resolveAgentRelayBrokerMcpArgs(args: { args.log('warn', 'harness.relay_mcp.broker_args_failed', { broker, exitCode: result.exitCode, - stderr: result.stderr.trim() + stderr: redactRelayBrokerOutput(result.stderr.trim(), args.relayMcp) }); return undefined; } @@ -657,7 +655,7 @@ async function resolveAgentRelayBrokerMcpArgs(args: { }); return undefined; } - if (!isBrokerMcpArgsOutput(parsed)) { + if (!isBrokerMcpArgsOutput(parsed) || parsed.args.length === 0) { args.log('warn', 'harness.relay_mcp.broker_args_invalid_shape', { broker }); return undefined; } @@ -708,7 +706,15 @@ function canExecuteFileSync(candidate: string): boolean { } function codexExistingArgs(args: string[]): string[] { - return args[0] === 'exec' ? args.slice(1) : [...args]; + return args[0] === 'exec' ? args.slice(1, -1) : [...args]; +} + +function redactRelayBrokerOutput(value: string, relayMcp: RelayMcpConfig): string { + let redacted = value; + for (const secret of [relayMcp.apiKey]) { + if (secret) redacted = redacted.replaceAll(secret, '[REDACTED]'); + } + return redacted; } function injectCodexSubcommandArgs(args: string[], injected: string[]): string[] { diff --git a/packages/runtime/src/runner.test.ts b/packages/runtime/src/runner.test.ts index 4e527cd4..a7ca13c8 100644 --- a/packages/runtime/src/runner.test.ts +++ b/packages/runtime/src/runner.test.ts @@ -276,6 +276,33 @@ async function writeFakeBroker(binDir: string, capturePath: string): Promise { + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(binDir, 'agent-relay-broker'), + [ + '#!/usr/bin/env node', + 'process.stdout.write(JSON.stringify({ args: [], sideEffectFiles: [] }));' + ].join('\n'), + 'utf8' + ); + await chmod(path.join(binDir, 'agent-relay-broker'), 0o755); +} + +async function writeFailingBroker(binDir: string): Promise { + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(binDir, 'agent-relay-broker'), + [ + '#!/usr/bin/env node', + "process.stderr.write(`failed for ${process.argv.join(' ')}\\n`);", + 'process.exit(2);' + ].join('\n'), + 'utf8' + ); + await chmod(path.join(binDir, 'agent-relay-broker'), 0o755); +} + test('cloud default codex harness injects agent-relay MCP args from broker helper', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); try { @@ -358,8 +385,9 @@ test('cloud default codex harness injects agent-relay MCP args from broker helpe assert.notEqual(existingArgsIdx, -1); assert.deepEqual( JSON.parse(brokerCaptured.argv[existingArgsIdx + 1]), - captured.argv.slice(11) + captured.argv.slice(11, -1) ); + assert.equal(captured.argv.at(-1), 'coordinate with the team\n\nUser task:\ndo the work'); assert.equal( brokerCaptured.argv[brokerCaptured.argv.indexOf('--workspaces-json') + 1], '{"workspaces":[{"id":"ws-relay"}]}' @@ -373,6 +401,135 @@ test('cloud default codex harness injects agent-relay MCP args from broker helpe } }); +test('cloud default broker args omit base URL when RELAY_BASE_URL is unset', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'codex-argv.json'); + const brokerCapturePath = path.join(tempDir, 'broker-argv.json'); + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'codex', capturePath); + await writeFakeBroker(binDir, brokerCapturePath); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + harness: 'codex', + model: 'openai/gpt-5', + systemPrompt: 'coordinate with the team', + harnessSettings: { reasoning: 'medium', timeoutSeconds: 5 } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: () => {}, + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'codex-1' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + + const brokerCaptured = JSON.parse(await readFile(brokerCapturePath, 'utf8')) as { + argv: string[]; + }; + assert.equal(brokerCaptured.argv.includes('--base-url'), false); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('cloud default broker failure logs redact Relay API key', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'codex-argv.json'); + const logs: Array<{ level: string; message: string; data?: unknown }> = []; + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'codex', capturePath); + await writeFailingBroker(binDir); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + harness: 'codex', + model: 'openai/gpt-5', + systemPrompt: 'coordinate with the team', + harnessSettings: { reasoning: 'medium', timeoutSeconds: 5 } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: (level, message, data) => logs.push({ level, message, data }), + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'codex-1', + RELAY_BASE_URL: 'https://relay.example.test' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + const failure = logs.find((log) => log.message === 'harness.relay_mcp.broker_args_failed'); + assert.ok(failure); + const stderr = (failure.data as { stderr: string }).stderr; + assert.match(stderr, /\[REDACTED\]/); + assert.doesNotMatch(stderr, /rk_live_test/); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('cloud default codex harness falls back when broker returns no args', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); + try { + const binDir = path.join(tempDir, 'bin'); + const workspaceRoot = path.join(tempDir, 'workspace'); + const capturePath = path.join(tempDir, 'codex-argv.json'); + await mkdir(workspaceRoot, { recursive: true }); + await writeArgCaptureHarness(binDir, 'codex', capturePath); + await writeEmptyArgsBroker(binDir); + + const defaults = createCloudRuntimeDefaults({ + persona: { + ...persona, + harness: 'codex', + model: 'openai/gpt-5', + systemPrompt: 'coordinate with the team', + harnessSettings: { reasoning: 'medium', timeoutSeconds: 5 } + }, + agent: runtimeAgent, + deployment: runtimeDeployment, + workspaceId: 'ws-test', + log: () => {}, + env: { + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`, + WORKFORCE_SANDBOX_ROOT: workspaceRoot, + RELAY_API_KEY: 'rk_live_test', + RELAY_AGENT_NAME: 'codex-1', + RELAY_BASE_URL: 'https://relay.example.test' + } + }); + + const result = await defaults.harnessRunner({ prompt: 'do the work' }); + assert.equal(result.exitCode, 0); + + const captured = JSON.parse(await readFile(capturePath, 'utf8')) as { argv: string[] }; + assert.ok(captured.argv.includes('mcp_servers.relaycast.command="npx"')); + assert.equal(captured.argv.includes('mcp_servers.agent-relay.command="npx"'), false); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + test('cloud default claude harness merges agent-relay MCP config from broker helper', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'workforce-runtime-')); try {