UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

140 lines (139 loc) 5.88 kB
import { w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js"; import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js"; import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import "./io-bdCzpGWJ.js"; import { n as resolveSessionTarget, t as callSessionTargetGateway } from "./session-target-sh9V8Tm1.js"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; import { constants as constants$1, tmpdir } from "node:os"; //#region src/cli/attach-cli.ts function writeClaudeMcpConfig(mcpConfig) { const dir = mkdtempSync(join(tmpdir(), "openclaw-attach-")); const path = join(dir, ".mcp.json"); writeFileSync(path, JSON.stringify(mcpConfig, null, 2), { encoding: "utf8", mode: 384 }); return { path, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } async function registerAttachCli(program, _argv = process.argv) { program.command("attach").description("Attach Claude Code to a gateway session with scoped MCP tools").argument("[target]", "Control UI URL, host/agent/ref, short ref, or agent:... key").option("--session <key>", "Gateway session key to bind (default: main session)").option("--url <url>", "Gateway WebSocket URL").option("--token <token>", "Gateway token (if required)").option("--password <password>", "Gateway password (if required)").option("--tls-fingerprint <sha256>", "Expected Gateway TLS certificate fingerprint").option("--ttl <ms>", "Grant TTL in positive base-10 integer milliseconds (default: gateway policy)").option("--bin <path>", "Claude Code binary to spawn", "claude").option("--print-config", "Mint the grant + write the .mcp.json, print how to launch it, and exit without spawning", false).addHelpText("after", "\nExamples:\n openclaw attach Attach Claude Code to the main session\n openclaw attach movies-a1166b81 Attach to a short session reference\n openclaw attach --session agent:main:telegram:123 --ttl 600000\n openclaw attach --print-config Set up the grant + config and print how to launch it yourself\n").action(async (target, opts) => { if (target && opts.session) throw new Error("pass one session target: use either the positional target or --session"); let ttlMs; if (opts.ttl !== void 0) { ttlMs = parseStrictPositiveInteger(opts.ttl); if (ttlMs === void 0) { defaultRuntime.error(`--ttl must be a positive integer of milliseconds. Got: ${JSON.stringify(opts.ttl)}`); defaultRuntime.exit(1); return; } } const cfg = getRuntimeConfig(); const resolved = target ? await resolveSessionTarget({ raw: target, gateway: { config: cfg, url: opts.url, token: opts.token, password: opts.password, tlsFingerprint: opts.tlsFingerprint } }) : void 0; const gateway = resolved?.gateway ?? { config: cfg, url: opts.url, token: opts.token, password: opts.password, tlsFingerprint: opts.tlsFingerprint }; const globalAgentId = resolved?.sessionKey === "global" && resolved.parsed.kind === "url" ? resolved.parsed.agentId : void 0; const granted = await callSessionTargetGateway({ gateway, method: "attach.grant", request: { sessionKey: resolved?.sessionKey ?? opts.session, ...globalAgentId ? { agentId: globalAgentId } : {}, ttlMs }, requiredScope: "operator.admin" }); if (!granted || typeof granted.token !== "string" || typeof granted.sessionKey !== "string" || typeof granted.expiresAtMs !== "number" || !Number.isFinite(granted.expiresAtMs) || !granted.mcpConfig?.mcpServers || typeof granted.env !== "object" || granted.env === null) { defaultRuntime.error("attach.grant returned an unexpected response from the gateway."); defaultRuntime.exit(1); return; } const grant = granted; const { path: configPath, cleanup } = writeClaudeMcpConfig(grant.mcpConfig); const expiresAt = new Date(grant.expiresAtMs).toISOString(); const claudeArgs = [ "--strict-mcp-config", "--mcp-config", configPath ]; if (opts.printConfig) { defaultRuntime.log(JSON.stringify({ sessionKey: grant.sessionKey, expiresAt, env: grant.env, configPath, launch: [opts.bin, ...claudeArgs] }, null, 2)); defaultRuntime.log(`Grant is live until ${expiresAt} and auto-expires; it is not revoked here. Launch with the env above, then delete ${configPath} when done.`); return; } let revokePromise; const revokeOnce = () => revokePromise ??= (async () => { try { await callSessionTargetGateway({ gateway, method: "attach.revoke", request: { token: grant.token }, requiredScope: "operator.admin" }); } catch (error) { defaultRuntime.error(`Warning: failed to revoke attach grant; it remains live until ${expiresAt}. ${String(error)}`); } cleanup(); })(); defaultRuntime.log(`Attaching Claude Code to session ${grant.sessionKey} (grant expires ${expiresAt})…`); const child = spawn(opts.bin, claudeArgs, { stdio: "inherit", env: { ...process.env, ...grant.env } }); const onSigint = () => {}; const onSigterm = () => child.kill("SIGTERM"); const finish = (code) => { process.off("SIGINT", onSigint); process.off("SIGTERM", onSigterm); defaultRuntime.exit(code); }; child.on("error", (error) => { (async () => { defaultRuntime.error(`Failed to launch '${opts.bin}': ${String(error)}`); await revokeOnce(); finish(1); })(); }); child.on("exit", (code, signal) => { (async () => { await revokeOnce(); const signalCode = signal ? 128 + (constants$1.signals[signal] ?? 0) : null; finish(signalCode ?? code ?? 0); })(); }); process.on("SIGINT", onSigint); process.on("SIGTERM", onSigterm); }); } //#endregion export { registerAttachCli, writeClaudeMcpConfig };