import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { clearRuntimeConfigSnapshot, type OpenClawConfig } from "../src/config/config.js"; import { GatewayClient } from "../src/gateway/client.js"; import { startGatewayServer } from "../src/gateway/server.js"; import { extractPayloadText } from "../src/gateway/test-helpers.agent-results.js"; import { getFreePortBlockWithPermissionFallback } from "../src/test-utils/ports.js"; import { GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js"; const DEFAULT_CLAUDE_ARGS = [ "-p", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--permission-mode", "bypassPermissions", ]; const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"]; const CLI_BOOTSTRAP_TIMEOUT_MS = 300_000; const GATEWAY_CONNECT_TIMEOUT_MS = 30_000; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function withMcpConfigOverrides(args: string[], mcpConfigPath: string): string[] { const next = [...args]; if (!next.includes("--strict-mcp-config")) { next.push("--strict-mcp-config"); } if (!next.includes("--mcp-config")) { next.push("--mcp-config", mcpConfigPath); } return next; } async function connectClient(params: { url: string; token: string }) { const startedAt = Date.now(); let attempt = 0; let lastError: Error | null = null; while (Date.now() - startedAt < GATEWAY_CONNECT_TIMEOUT_MS) { attempt += 1; const remainingMs = GATEWAY_CONNECT_TIMEOUT_MS - (Date.now() - startedAt); if (remainingMs <= 0) { break; } try { return await connectClientOnce({ ...params, timeoutMs: Math.min(remainingMs, 10_000), }); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (!isRetryableGatewayConnectError(lastError) || remainingMs <= 2_000) { throw lastError; } await sleep(Math.min(500 * attempt, 2_000)); } } throw lastError ?? new Error("gateway connect timeout"); } async function connectClientOnce(params: { url: string; token: string; timeoutMs: number }) { return await new Promise((resolve, reject) => { let done = false; let client: GatewayClient | undefined; const finish = (result: { client?: GatewayClient; error?: Error }) => { if (done) { return; } done = true; clearTimeout(connectTimeout); if (result.error) { if (client) { void client.stopAndWait({ timeoutMs: 1_000 }).catch(() => {}); } reject(result.error); return; } resolve(result.client as GatewayClient); }; client = new GatewayClient({ url: params.url, token: params.token, clientName: GATEWAY_CLIENT_NAMES.TEST, clientVersion: "dev", mode: "test", requestTimeoutMs: params.timeoutMs, connectChallengeTimeoutMs: params.timeoutMs, onHelloOk: () => finish({ client }), onConnectError: (error) => finish({ error }), onClose: (code, reason) => finish({ error: new Error(`gateway closed during connect (${code}): ${reason}`) }), }); const connectTimeout = setTimeout( () => finish({ error: new Error("gateway connect timeout") }), params.timeoutMs, ); connectTimeout.unref(); client.start(); }); } function isRetryableGatewayConnectError(error: Error): boolean { const message = error.message.toLowerCase(); return ( message.includes("gateway closed during connect (1000)") || message.includes("gateway connect timeout") || message.includes("gateway connect challenge timeout") ); } async function getFreeGatewayPort(): Promise { return await getFreePortBlockWithPermissionFallback({ offsets: [0, 1, 2, 4], fallbackBase: 40_000, }); } async function main() { const preservedEnv = new Set( JSON.parse(process.env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV ?? "[]") as string[], ); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-inline-bootstrap-")); const workspaceRootDir = path.join(tempDir, "workspace"); const workspaceDir = path.join(workspaceRootDir, "dev"); const soulSecret = `SOUL-${randomUUID()}`; const identitySecret = `IDENTITY-${randomUUID()}`; const userSecret = `USER-${randomUUID()}`; await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile( path.join(workspaceDir, "AGENTS.md"), [ "# AGENTS.md", "", "When the user sends a BOOTSTRAP_CHECK token, reply with exactly:", `BOOTSTRAP_OK ${soulSecret} ${identitySecret} ${userSecret}`, "Do not add any other words or punctuation.", ].join("\n"), ); await fs.writeFile(path.join(workspaceDir, "SOUL.md"), `${soulSecret}\n`); await fs.writeFile(path.join(workspaceDir, "IDENTITY.md"), `${identitySecret}\n`); await fs.writeFile(path.join(workspaceDir, "USER.md"), `${userSecret}\n`); const cfg: OpenClawConfig = {}; const existingBackends = cfg.agents?.defaults?.cliBackends ?? {}; const claudeBackend = existingBackends["claude-cli"] ?? {}; const cliCommand = process.env.OPENCLAW_LIVE_CLI_BACKEND_COMMAND ?? claudeBackend.command ?? "claude"; let cliArgs = claudeBackend.args ?? DEFAULT_CLAUDE_ARGS; const mcpConfigPath = path.join(tempDir, "claude-mcp.json"); await fs.writeFile(mcpConfigPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`); cliArgs = withMcpConfigOverrides(cliArgs, mcpConfigPath); const cliClearEnv = (claudeBackend.clearEnv ?? DEFAULT_CLEAR_ENV).filter( (name) => !preservedEnv.has(name), ); const preservedCliEnv = Object.fromEntries( [...preservedEnv] .map((name) => [name, process.env[name]]) .filter((entry): entry is [string, string] => typeof entry[1] === "string"), ); const nextCfg = { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, workspace: workspaceRootDir, model: { primary: "claude-cli/claude-sonnet-4-6" }, models: { "claude-cli/claude-sonnet-4-6": {} }, cliBackends: { ...existingBackends, "claude-cli": { ...claudeBackend, command: cliCommand, args: cliArgs, clearEnv: cliClearEnv.length > 0 ? cliClearEnv : undefined, env: Object.keys(preservedCliEnv).length > 0 ? preservedCliEnv : undefined, systemPromptWhen: "first", }, }, sandbox: { mode: "off" }, }, }, }; const tempConfigPath = path.join(tempDir, "openclaw.json"); await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`); process.env.OPENCLAW_CONFIG_PATH = tempConfigPath; process.env.OPENCLAW_SKIP_CHANNELS = "1"; process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; process.env.OPENCLAW_SKIP_CRON = "1"; process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; const port = await getFreeGatewayPort(); const token = `test-${randomUUID()}`; process.env.OPENCLAW_GATEWAY_TOKEN = token; const server = await startGatewayServer(port, { bind: "loopback", auth: { mode: "token", token }, controlUiEnabled: false, }); const client = await connectClient({ url: `ws://127.0.0.1:${port}`, token }); try { const payload = await client.request( "agent", { sessionKey: `agent:dev:inline-cli-bootstrap-${randomUUID()}`, idempotencyKey: `idem-${randomUUID()}`, message: `BOOTSTRAP_CHECK ${randomUUID()}`, deliver: false, }, { expectFinal: true, timeoutMs: CLI_BOOTSTRAP_TIMEOUT_MS }, ); const text = extractPayloadText(payload?.result); process.stdout.write( `${JSON.stringify({ ok: true, text, expectedText: `BOOTSTRAP_OK ${soulSecret} ${identitySecret} ${userSecret}`, systemPromptReport: payload?.result?.meta?.systemPromptReport ?? null, })}\n`, ); } finally { await client.stopAndWait(); await server.close({ reason: "bootstrap live probe done" }); await fs.rm(tempDir, { recursive: true, force: true }); clearRuntimeConfigSnapshot(); } } try { await main(); process.exit(0); } catch (error) { process.stderr.write(`${String(error)}\n`); process.exit(1); }