openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
140 lines (139 loc) • 5.88 kB
JavaScript
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 };