import { spawn } from "node:child_process"; // Live prompt probe for Anthropic setup-token and Claude CLI prompt-path debugging. // Usage: // OPENCLAW_PROMPT_TRANSPORT=direct|gateway // OPENCLAW_PROMPT_MODE=extra|override // OPENCLAW_PROMPT_TEXT='...' // OPENCLAW_PROMPT_CAPTURE=1 // pnpm probe:anthropic:prompt import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import http from "node:http"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { resolveOpenClawAgentDir } from "../src/agents/agent-paths.js"; import { ensureAuthProfileStore, type AuthProfileCredential } from "../src/agents/auth-profiles.js"; import { normalizeProviderId } from "../src/agents/model-selection.js"; import { validateAnthropicSetupToken } from "../src/commands/auth-token.js"; import { callGateway } from "../src/gateway/call.js"; import { extractPayloadText } from "../src/gateway/test-helpers.agent-results.js"; import { getFreePortBlockWithPermissionFallback } from "../src/test-utils/ports.js"; const TRANSPORT = process.env.OPENCLAW_PROMPT_TRANSPORT?.trim() === "direct" ? "direct" : "gateway"; const GATEWAY_PROMPT_MODE = process.env.OPENCLAW_PROMPT_MODE?.trim() === "override" ? "override" : "extra"; const PROMPT_TEXT = process.env.OPENCLAW_PROMPT_TEXT?.trim() ?? ""; const PROMPT_LIST_JSON = process.env.OPENCLAW_PROMPT_LIST_JSON?.trim() ?? ""; const USER_PROMPT = process.env.OPENCLAW_USER_PROMPT?.trim() || "is clawd here?"; const ENABLE_CAPTURE = process.env.OPENCLAW_PROMPT_CAPTURE === "1"; const INCLUDE_RAW = process.env.OPENCLAW_PROMPT_INCLUDE_RAW === "1"; const CLAUDE_BIN = process.env.CLAUDE_BIN?.trim() || "claude"; const NODE_BIN = process.env.OPENCLAW_NODE_BIN?.trim() || process.execPath; const TIMEOUT_MS = Number(process.env.OPENCLAW_PROMPT_TIMEOUT_MS ?? "45000"); const GATEWAY_TIMEOUT_MS = Number(process.env.OPENCLAW_PROMPT_GATEWAY_TIMEOUT_MS ?? "120000"); const SETUP_TOKEN_RAW = process.env.OPENCLAW_LIVE_SETUP_TOKEN?.trim() ?? ""; const SETUP_TOKEN_VALUE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE?.trim() ?? ""; const SETUP_TOKEN_PROFILE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? ""; const DIRECT_CLAUDE_ARGS = ["-p", "--append-system-prompt"]; if (!PROMPT_TEXT && !PROMPT_LIST_JSON) { throw new Error("missing OPENCLAW_PROMPT_TEXT or OPENCLAW_PROMPT_LIST_JSON"); } type CaptureSummary = { url?: string; authScheme?: string; xApp?: string; anthropicBeta?: string; systemBlockCount: number; systemBlocks: Array<{ index: number; bytes: number; preview: string }>; containsPromptExact: boolean; bodyContainsPromptExact: boolean; userBytes?: number; userPreview?: string; rawBody?: string; }; type PromptResult = { prompt: string; ok: boolean; transport: "direct" | "gateway"; promptMode?: "extra" | "override"; exitCode?: number | null; signal?: NodeJS.Signals | null; status?: string; text?: string; stdout?: string; stderr?: string; error?: string; matchedExtraUsage400: boolean; capture?: CaptureSummary; tmpDir: string; }; type ProxyCapture = { url?: string; authHeader?: string; xApp?: string; anthropicBeta?: string; systemTexts: string[]; userText?: string; rawBody?: string; }; type TokenSource = { profileId: string; token: string; }; function toHeaderValue(value: string | string[] | undefined): string | undefined { return Array.isArray(value) ? value.join(", ") : value; } function summarizeText(text: string, max = 120): string { const normalized = text.replace(/\s+/g, " ").trim(); if (normalized.length <= max) { return normalized; } return `${normalized.slice(0, max - 1)}…`; } function summarizeCapture( capture: ProxyCapture | undefined, prompt: string, ): CaptureSummary | undefined { if (!capture) { return undefined; } return { url: capture.url, authScheme: capture.authHeader?.split(/\s+/, 1)[0], xApp: capture.xApp, anthropicBeta: capture.anthropicBeta, systemBlockCount: capture.systemTexts.length, systemBlocks: capture.systemTexts.map((entry, index) => ({ index, bytes: Buffer.byteLength(entry, "utf8"), preview: summarizeText(entry), })), containsPromptExact: capture.systemTexts.includes(prompt), bodyContainsPromptExact: capture.rawBody?.includes(prompt) ?? false, userBytes: capture.userText ? Buffer.byteLength(capture.userText, "utf8") : undefined, userPreview: capture.userText ? summarizeText(capture.userText) : undefined, rawBody: INCLUDE_RAW ? capture.rawBody : undefined, }; } function matchesExtraUsage400(...parts: Array): boolean { return parts .filter((value): value is string => typeof value === "string" && value.length > 0) .join(" ") .toLowerCase() .includes("third-party apps now draw from your extra usage"); } function isSetupToken(value: string): boolean { return value.startsWith("sk-ant-oat01-"); } function listSetupTokenProfiles(store: { profiles: Record; }): Array<{ id: string; token: string }> { return Object.entries(store.profiles) .filter(([, cred]) => { if (cred.type !== "token") { return false; } if (normalizeProviderId(cred.provider) !== "anthropic") { return false; } return isSetupToken(cred.token ?? ""); }) .map(([id, cred]) => ({ id, token: cred.token ?? "" })); } function pickSetupTokenProfile(candidates: Array<{ id: string; token: string }>): { id: string; token: string; } | null { const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"]; for (const id of preferred) { const match = candidates.find((entry) => entry.id === id); if (match) { return match; } } return candidates[0] ?? null; } function validateSetupToken(value: string): string { const error = validateAnthropicSetupToken(value); if (error) { throw new Error(`invalid setup-token: ${error}`); } return value; } function resolveSetupTokenSource(): TokenSource { const explicitToken = (SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE; if (explicitToken) { return { profileId: "anthropic:default", token: validateSetupToken(explicitToken), }; } const agentDir = resolveOpenClawAgentDir(); const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, }); const candidates = listSetupTokenProfiles(store); if (SETUP_TOKEN_PROFILE) { const match = candidates.find((entry) => entry.id === SETUP_TOKEN_PROFILE); if (!match) { throw new Error(`setup-token profile not found: ${SETUP_TOKEN_PROFILE}`); } return { profileId: match.id, token: validateSetupToken(match.token) }; } const match = pickSetupTokenProfile(candidates); if (!match) { throw new Error( "no Anthropics setup-token profile found; set OPENCLAW_LIVE_SETUP_TOKEN_VALUE or OPENCLAW_LIVE_SETUP_TOKEN_PROFILE", ); } return { profileId: match.id, token: validateSetupToken(match.token) }; } async function sleep(ms: number): Promise { return await new Promise((resolve) => setTimeout(resolve, ms)); } async function withTimeout( promise: Promise, timeoutMs: number, fallback: () => T, ): Promise { return await Promise.race([promise, sleep(timeoutMs).then(() => fallback())]); } async function readRequestBody(req: http.IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks); } function extractProxyCapture(rawBody: string, req: http.IncomingMessage): ProxyCapture { let parsed: { system?: Array<{ text?: string }>; messages?: Array<{ role?: string; content?: unknown }>; } | null = null; try { parsed = JSON.parse(rawBody) as typeof parsed; } catch { parsed = null; } const systemTexts = Array.isArray(parsed?.system) ? parsed.system .map((entry) => (typeof entry?.text === "string" ? entry.text : "")) .filter(Boolean) : []; const userText = Array.isArray(parsed?.messages) ? parsed.messages .filter((entry) => entry?.role === "user") .flatMap((entry) => { const content = entry?.content; if (typeof content === "string") { return [content]; } if (!Array.isArray(content)) { return []; } return content .map((item) => item && typeof item === "object" && "text" in item && typeof item.text === "string" ? item.text : "", ) .filter(Boolean); }) .join("\n") : undefined; return { url: req.url ?? undefined, authHeader: toHeaderValue(req.headers.authorization), xApp: toHeaderValue(req.headers["x-app"]), anthropicBeta: toHeaderValue(req.headers["anthropic-beta"]), systemTexts, userText, rawBody, }; } async function startAnthropicProxy(params: { port: number; upstreamBaseUrl: string }) { let lastCapture: ProxyCapture | undefined; const sockets = new Set(); const server = http.createServer(async (req, res) => { try { const method = req.method ?? "GET"; const requestBody = await readRequestBody(req); const rawBody = requestBody.toString("utf8"); lastCapture = extractProxyCapture(rawBody, req); const upstreamUrl = new URL(req.url ?? "/", params.upstreamBaseUrl).toString(); const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value === undefined) { continue; } const lower = key.toLowerCase(); if (lower === "host" || lower === "content-length") { continue; } headers.set(key, Array.isArray(value) ? value.join(", ") : value); } const upstreamRes = await fetch(upstreamUrl, { method, headers, body: method === "GET" || method === "HEAD" || requestBody.byteLength === 0 ? undefined : requestBody, duplex: "half", }); const responseHeaders: Record = {}; for (const [key, value] of upstreamRes.headers.entries()) { const lower = key.toLowerCase(); if ( lower === "content-length" || lower === "content-encoding" || lower === "transfer-encoding" || lower === "connection" || lower === "keep-alive" ) { continue; } responseHeaders[key] = value; } res.writeHead(upstreamRes.status, responseHeaders); if (upstreamRes.body) { for await (const chunk of upstreamRes.body) { res.write(Buffer.from(chunk)); } } res.end(); } catch (error) { res.writeHead(502, { "content-type": "text/plain; charset=utf-8" }); res.end(`proxy error: ${String(error)}`); } }); server.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(params.port, "127.0.0.1", () => resolve()); }); return { getLastCapture() { return lastCapture; }, async stop() { for (const socket of sockets) { socket.destroy(); } await withTimeout( new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }), 1_000, () => undefined, ); }, }; } async function getFreePort(): Promise { return await getFreePortBlockWithPermissionFallback({ offsets: [0, 1, 2, 4], fallbackBase: 44_000, }); } async function runDirectPrompt(prompt: string): Promise { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-direct-prompt-probe-")); const proxyPort = ENABLE_CAPTURE ? await getFreePort() : undefined; const proxy = ENABLE_CAPTURE && proxyPort ? await startAnthropicProxy({ port: proxyPort, upstreamBaseUrl: "https://api.anthropic.com" }) : undefined; const stdout: string[] = []; const stderr: string[] = []; const child = spawn(CLAUDE_BIN, [...DIRECT_CLAUDE_ARGS, prompt, USER_PROMPT], { cwd: process.cwd(), env: { ...process.env, ...(proxyPort ? { ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxyPort}` } : {}), ANTHROPIC_API_KEY: "", ANTHROPIC_API_KEY_OLD: "", }, stdio: ["ignore", "pipe", "pipe"], }); child.stdout.on("data", (chunk) => stdout.push(String(chunk))); child.stderr.on("data", (chunk) => stderr.push(String(chunk))); const exit = await withTimeout( new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => { child.once("exit", (code, signal) => resolve({ code, signal })); }), TIMEOUT_MS, () => { child.kill("SIGKILL"); return { code: null, signal: "SIGKILL" as NodeJS.Signals }; }, ); await proxy?.stop().catch(() => {}); const joinedStdout = stdout.join(""); const joinedStderr = stderr.join(""); return { prompt, ok: exit.code === 0 && !matchesExtraUsage400(joinedStdout, joinedStderr), transport: "direct", exitCode: exit.code, signal: exit.signal, stdout: joinedStdout.trim() || undefined, stderr: joinedStderr.trim() || undefined, matchedExtraUsage400: matchesExtraUsage400(joinedStdout, joinedStderr), capture: summarizeCapture(proxy?.getLastCapture(), prompt), tmpDir, }; } async function startGatewayProcess(params: { port: number; gatewayToken: string; configPath: string; stateDir: string; agentDir: string; bundledPluginsDir: string; logPath: string; }) { const logFile = await fs.open(params.logPath, "a"); const child = spawn( NODE_BIN, ["openclaw.mjs", "gateway", "--port", String(params.port), "--bind", "loopback", "--force"], { cwd: process.cwd(), env: { ...process.env, OPENCLAW_CONFIG_PATH: params.configPath, OPENCLAW_STATE_DIR: params.stateDir, OPENCLAW_AGENT_DIR: params.agentDir, OPENCLAW_GATEWAY_TOKEN: params.gatewayToken, OPENCLAW_SKIP_CHANNELS: "1", OPENCLAW_SKIP_GMAIL_WATCHER: "1", OPENCLAW_SKIP_CANVAS_HOST: "1", OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1", OPENCLAW_DISABLE_BONJOUR: "1", OPENCLAW_SKIP_CRON: "1", OPENCLAW_TEST_MINIMAL_GATEWAY: "1", OPENCLAW_BUNDLED_PLUGINS_DIR: params.bundledPluginsDir, ANTHROPIC_API_KEY: "", ANTHROPIC_API_KEY_OLD: "", }, stdio: ["ignore", "pipe", "pipe"], }, ); child.stdout.on("data", (chunk) => void logFile.appendFile(chunk)); child.stderr.on("data", (chunk) => void logFile.appendFile(chunk)); return { async stop() { if (!child.killed) { child.kill("SIGINT"); } const exited = await withTimeout( new Promise((resolve) => child.once("exit", () => resolve(true))), 1_500, () => false, ); if (!exited && !child.killed) { child.kill("SIGKILL"); } await logFile.close(); }, }; } async function waitForGatewayReady(url: string, token: string): Promise { const deadline = Date.now() + 45_000; let lastError = "gateway start timeout"; while (Date.now() < deadline) { try { await callGateway({ url, token, method: "health", timeoutMs: 5_000 }); return; } catch (error) { lastError = String(error); await sleep(500); } } throw new Error(lastError); } async function readLogTail(logPath: string): Promise { const raw = await fs.readFile(logPath, "utf8").catch(() => ""); return raw.split(/\r?\n/).slice(-40).join("\n").trim(); } async function runGatewayPrompt(prompt: string): Promise { const tokenSource = resolveSetupTokenSource(); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-prompt-probe-")); const stateDir = path.join(tmpDir, "state"); const agentDir = path.join(stateDir, "agents", "main", "agent"); const bundledPluginsDir = path.join(tmpDir, "bundled-plugins-empty"); const configPath = path.join(tmpDir, "openclaw.json"); const logPath = path.join(tmpDir, "gateway.log"); const gatewayToken = `gw-${randomUUID()}`; const port = await getFreePort(); const proxyPort = ENABLE_CAPTURE ? await getFreePort() : undefined; const proxy = ENABLE_CAPTURE && proxyPort ? await startAnthropicProxy({ port: proxyPort, upstreamBaseUrl: "https://api.anthropic.com" }) : undefined; await fs.mkdir(agentDir, { recursive: true }); await fs.mkdir(bundledPluginsDir, { recursive: true }); await fs.writeFile( configPath, `${JSON.stringify( { gateway: { mode: "local", controlUi: { enabled: false }, tailscale: { mode: "off" }, }, discovery: { mdns: { mode: "off" }, wideArea: { enabled: false }, }, ...(proxyPort ? { models: { providers: { anthropic: { baseUrl: `http://127.0.0.1:${proxyPort}`, api: "anthropic-messages", models: [], }, }, }, } : {}), auth: { profiles: { [tokenSource.profileId]: { provider: "anthropic", mode: "token" } }, order: { anthropic: [tokenSource.profileId] }, }, agents: { defaults: { model: "anthropic/claude-sonnet-4-6", heartbeat: { includeSystemPromptSection: false, }, ...(GATEWAY_PROMPT_MODE === "override" ? { systemPromptOverride: prompt } : {}), }, }, }, null, 2, )}\n`, ); await fs.writeFile( path.join(agentDir, "auth-profiles.json"), `${JSON.stringify( { version: 1, profiles: { [tokenSource.profileId]: { type: "token", provider: "anthropic", token: tokenSource.token, }, }, }, null, 2, )}\n`, ); const gateway = await startGatewayProcess({ port, gatewayToken, configPath, stateDir, agentDir, bundledPluginsDir, logPath, }); try { const url = `ws://127.0.0.1:${port}`; await waitForGatewayReady(url, gatewayToken); const agentRes = await callGateway<{ runId?: string }>({ url, token: gatewayToken, method: "agent", params: { sessionKey: `agent:main:prompt-probe-${randomUUID()}`, idempotencyKey: `idem-${randomUUID()}`, message: "Reply with exactly: PROMPT PROBE OK.", ...(GATEWAY_PROMPT_MODE === "extra" ? { extraSystemPrompt: prompt } : {}), deliver: false, }, timeoutMs: 15_000, clientName: "cli", mode: "cli", }); if (typeof agentRes.runId !== "string" || agentRes.runId.trim().length === 0) { return { prompt, ok: false, transport: "gateway", promptMode: GATEWAY_PROMPT_MODE, error: `missing runId: ${JSON.stringify(agentRes)}`, matchedExtraUsage400: false, capture: summarizeCapture(proxy?.getLastCapture(), prompt), tmpDir, }; } const waitRes = await callGateway<{ status?: string; error?: string; payloads?: unknown[] }>({ url, token: gatewayToken, method: "agent.wait", params: { runId: agentRes.runId, timeoutMs: GATEWAY_TIMEOUT_MS }, timeoutMs: GATEWAY_TIMEOUT_MS + 10_000, clientName: "cli", mode: "cli", }); const text = extractPayloadText(waitRes); const logTail = await readLogTail(logPath); const matched400 = matchesExtraUsage400(waitRes.error, logTail, JSON.stringify(waitRes)); return { prompt, ok: waitRes.status === "ok" && !matched400, transport: "gateway", promptMode: GATEWAY_PROMPT_MODE, status: waitRes.status, text: text || undefined, error: waitRes.status === "ok" ? undefined : waitRes.error || logTail || "agent.wait failed", matchedExtraUsage400: matched400, capture: summarizeCapture(proxy?.getLastCapture(), prompt), tmpDir, }; } finally { await gateway.stop().catch(() => {}); await proxy?.stop().catch(() => {}); } } async function main() { const prompts = PROMPT_LIST_JSON ? (JSON.parse(PROMPT_LIST_JSON) as string[]) : [PROMPT_TEXT]; const results: PromptResult[] = []; for (const prompt of prompts) { results.push( TRANSPORT === "direct" ? await runDirectPrompt(prompt) : await runGatewayPrompt(prompt), ); } console.log( JSON.stringify( { transport: TRANSPORT, ...(TRANSPORT === "gateway" ? { promptMode: GATEWAY_PROMPT_MODE } : {}), capture: ENABLE_CAPTURE, results, }, null, 2, ), ); } await main();