UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

233 lines (232 loc) 9.01 kB
import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "../number-coercion-CJQ8TR--.js"; import { y as resolveStateDir } from "../paths-mvMm5bYV.js"; import { m as writeJson, o as tryReadJson } from "../json-files-2umMHm0W.js"; import { u as normalizeAgentId } from "../session-key-B_NoIfpX.js"; import { a as parseCrestodianOperation, i as isPersistentCrestodianOperation, n as executeCrestodianOperation, r as formatCrestodianPersistentPlan } from "../operations-B8_IoqaI.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash, randomUUID } from "node:crypto"; //#region src/crestodian/rescue-policy.ts function resolvePendingTtlMinutes(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 15; } function resolveAgentEntry(cfg, agentId) { if (!agentId) return; const id = normalizeAgentId(agentId); return cfg.agents?.list?.find((entry) => entry !== null && typeof entry === "object" && normalizeAgentId(entry.id) === id); } function resolveScopedExecConfig(cfg, agentId) { return resolveAgentEntry(cfg, agentId)?.tools?.exec; } function resolveScopedSandboxMode(cfg, agentId) { return resolveAgentEntry(cfg, agentId)?.sandbox?.mode ?? cfg.agents?.defaults?.sandbox?.mode ?? "off"; } function isYoloHostPosture(cfg, agentId) { const scopedExec = resolveScopedExecConfig(cfg, agentId); const globalExec = cfg.tools?.exec; const security = scopedExec?.security ?? globalExec?.security ?? "full"; const ask = scopedExec?.ask ?? globalExec?.ask ?? "off"; return security === "full" && ask === "off"; } /** Decide whether a message-channel rescue command is allowed for this sender/context. */ function resolveCrestodianRescuePolicy(input) { const rescue = input.cfg.crestodian?.rescue; const configuredEnabled = rescue?.enabled ?? "auto"; const ownerDmOnly = rescue?.ownerDmOnly ?? true; const pendingTtlMinutes = resolvePendingTtlMinutes(rescue?.pendingTtlMinutes); const sandboxActive = resolveScopedSandboxMode(input.cfg, input.agentId) !== "off"; const yolo = !sandboxActive && isYoloHostPosture(input.cfg, input.agentId); const enabled = configuredEnabled === "auto" ? yolo : configuredEnabled; if (!enabled) return { allowed: false, enabled, ownerDmOnly, pendingTtlMinutes, yolo, sandboxActive, reason: "disabled", message: "Crestodian rescue is disabled. Set crestodian.rescue.enabled=true or use YOLO host posture with sandboxing off." }; if (sandboxActive) return { allowed: false, enabled, ownerDmOnly, pendingTtlMinutes, yolo, sandboxActive, reason: "sandbox-active", message: "Crestodian rescue is blocked because OpenClaw sandboxing is active. Fix the install locally or disable sandboxing before using remote rescue." }; if (configuredEnabled === "auto" && !yolo) return { allowed: false, enabled, ownerDmOnly, pendingTtlMinutes, yolo, sandboxActive, reason: "not-yolo", message: "Crestodian rescue auto-mode only opens in YOLO host posture: tools.exec.security=full, tools.exec.ask=off, and sandboxing off." }; if (!input.senderIsOwner) return { allowed: false, enabled, ownerDmOnly, pendingTtlMinutes, yolo, sandboxActive, reason: "not-owner", message: "Crestodian rescue only accepts commands from an OpenClaw owner." }; if (ownerDmOnly && !input.isDirectMessage) return { allowed: false, enabled, ownerDmOnly, pendingTtlMinutes, yolo, sandboxActive, reason: "not-direct-message", message: "Crestodian rescue is restricted to owner DMs by default." }; return { allowed: true, enabled: true, ownerDmOnly, pendingTtlMinutes, yolo: true, sandboxActive: false }; } //#endregion //#region src/crestodian/rescue-message.ts const CRESTODIAN_COMMAND = "/crestodian"; const APPROVAL_RE = /^(yes|y|apply|approve|approved|do it)$/i; function createCaptureRuntime() { const lines = []; const push = (...args) => { lines.push(args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")); }; return { runtime: { log: push, error: push, exit: (code) => { throw new Error(`Crestodian operation exited with code ${code}`); } }, read: () => lines.join("\n").trim() }; } /** Extract the command body after `/crestodian`, or null when the message is not for rescue. */ function extractCrestodianRescueMessage(commandBody) { const normalized = commandBody.trim(); const lower = normalized.toLowerCase(); if (lower !== CRESTODIAN_COMMAND && !lower.startsWith(`${CRESTODIAN_COMMAND} `)) return null; return normalized.slice(11).trim(); } function resolvePendingDir(env = process.env) { return path.join(resolveStateDir(env), "crestodian", "rescue-pending"); } function resolvePendingPath(input) { const key = JSON.stringify({ channel: input.command.channelId ?? input.command.channel, from: input.command.from, senderId: input.command.senderId }); const digest = createHash("sha256").update(key).digest("hex").slice(0, 32); return path.join(resolvePendingDir(input.env), `${digest}.json`); } async function readPending(pendingPath, now = /* @__PURE__ */ new Date()) { try { const parsed = await tryReadJson(pendingPath); if (!parsed) return null; const expiresAtMs = asDateTimestampMs(Date.parse(parsed.expiresAt)); const nowMs = asDateTimestampMs(now.getTime()); if (expiresAtMs === void 0 || nowMs === void 0 || expiresAtMs <= nowMs) { await fs.rm(pendingPath, { force: true }); return null; } return parsed; } catch { return null; } } async function writePending(pendingPath, pending) { await writeJson(pendingPath, pending, { dirMode: 448, mode: 384, trailingNewline: true }); } function buildAuditDetails(input) { return { rescue: true, channel: input.command.channelId ?? input.command.channel, accountId: input.command.to, senderId: input.command.senderId, from: input.command.from }; } function formatPersistentPlan(operation) { return formatCrestodianPersistentPlan(operation).replace("Say yes to apply.", "Reply /crestodian yes to apply."); } function formatUnsupportedRemoteOperation(operation) { if (operation.kind === "open-tui") return ["Crestodian rescue cannot open the local TUI from a message channel.", "Use local `openclaw` for agent handoff, or ask for status, doctor, config, gateway, agents, or models."].join(" "); if (operation.kind === "plugin-install") return ["Crestodian rescue cannot install plugins from a message channel by default because plugin install downloads executable code.", "Use local `openclaw crestodian` or `openclaw plugins install` instead."].join(" "); return null; } /** Process one rescue message and return a reply, or null when not a rescue command. */ async function runCrestodianRescueMessage(input) { const rescueMessage = extractCrestodianRescueMessage(input.commandBody); if (rescueMessage === null) return null; const policy = resolveCrestodianRescuePolicy({ cfg: input.cfg, agentId: input.agentId, senderIsOwner: input.command.senderIsOwner, isDirectMessage: !input.isGroup }); if (!policy.allowed) return policy.message; const pendingPath = resolvePendingPath(input); if (APPROVAL_RE.test(rescueMessage)) { const pending = await readPending(pendingPath); if (!pending) return "No pending Crestodian rescue change is waiting for approval."; const unsupported = formatUnsupportedRemoteOperation(pending.operation); if (unsupported) { await fs.rm(pendingPath, { force: true }); return unsupported; } const capture = createCaptureRuntime(); await executeCrestodianOperation(pending.operation, capture.runtime, { approved: true, auditDetails: pending.auditDetails, deps: input.deps }); await fs.rm(pendingPath, { force: true }); return capture.read() || "Crestodian rescue change applied."; } const operation = parseCrestodianOperation(rescueMessage); const unsupported = formatUnsupportedRemoteOperation(operation); if (unsupported) return unsupported; if (isPersistentCrestodianOperation(operation)) { const now = /* @__PURE__ */ new Date(); const nowMs = asDateTimestampMs(now.getTime()); const expiresAtMs = nowMs === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(policy.pendingTtlMinutes * 6e4, { nowMs }); if (expiresAtMs === void 0) return "Crestodian rescue could not create a pending approval because the expiry clock is invalid."; await writePending(pendingPath, { id: randomUUID(), createdAt: now.toISOString(), expiresAt: new Date(expiresAtMs).toISOString(), operation, auditDetails: buildAuditDetails(input) }); return formatPersistentPlan(operation); } const capture = createCaptureRuntime(); await executeCrestodianOperation(operation, capture.runtime, { approved: true, auditDetails: buildAuditDetails(input), deps: input.deps }); return capture.read() || "Crestodian listened, clicked a claw, and found nothing to change."; } //#endregion export { extractCrestodianRescueMessage, runCrestodianRescueMessage };