openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
261 lines (260 loc) • 11.4 kB
JavaScript
import { D as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "../number-coercion-CLj0HTDM.js";
import { t as hasNonEmptyString } from "../string-coerce-CIXf7egm.js";
import { d as resolveAgentEntry } from "../agent-scope-config-DcbEhP0R.js";
import { t as createCorePluginStateSyncKeyedStore } from "../plugin-state-store-C6hmUuuk.js";
import { x as resolveExecModePolicy } from "../exec-approvals-policy-CCFUzTzd.js";
import "../exec-approvals-BSZ-fPIY.js";
import { a as isPersistentSystemAgentOperation, i as formatSystemAgentPersistentPlan, o as parseSystemAgentOperation } from "../operations-parse-DIiMdt6S.js";
import { t as executeSystemAgentOperation } from "../operations-BAC9TFg6.js";
import { t as classifySystemAgentApprovalText } from "../operator-approval-BAOcpwRx.js";
import { createHash } from "node:crypto";
//#region src/system-agent/rescue-policy.ts
function resolveScopedExecConfig(cfg, agentId) {
return agentId ? resolveAgentEntry(cfg, agentId)?.tools?.exec : void 0;
}
function resolveScopedSandboxMode(cfg, agentId) {
return (agentId ? resolveAgentEntry(cfg, agentId)?.sandbox?.mode : void 0) ?? cfg.agents?.defaults?.sandbox?.mode ?? "off";
}
function isYoloHostPosture(cfg, agentId) {
const scopedExec = resolveScopedExecConfig(cfg, agentId);
const globalExec = cfg.tools?.exec;
const inherited = resolveExecModePolicy({
mode: globalExec?.mode,
security: globalExec?.security ?? "full",
ask: globalExec?.ask ?? "off"
});
return resolveExecModePolicy({
mode: scopedExec?.mode,
security: scopedExec?.security ?? inherited.security,
ask: scopedExec?.ask ?? inherited.ask
}).mode === "full";
}
/** Decide whether a message-channel rescue command is allowed for this sender/context. */
function resolveSystemAgentRescuePolicy(input) {
const ownerDmOnly = true;
const pendingTtlMinutes = 15;
const sandboxActive = resolveScopedSandboxMode(input.cfg, input.agentId) !== "off";
const yolo = !sandboxActive && isYoloHostPosture(input.cfg, input.agentId);
const enabled = yolo;
if (sandboxActive) return {
allowed: false,
enabled,
ownerDmOnly,
pendingTtlMinutes,
yolo,
sandboxActive,
reason: "sandbox-active",
message: "OpenClaw rescue is blocked because OpenClaw sandboxing is active. Fix the install locally or disable sandboxing before using remote rescue."
};
if (!enabled) return {
allowed: false,
enabled,
ownerDmOnly,
pendingTtlMinutes,
yolo,
sandboxActive,
reason: "disabled",
message: "OpenClaw rescue requires YOLO host posture with sandboxing off."
};
if (!input.senderIsOwner) return {
allowed: false,
enabled,
ownerDmOnly,
pendingTtlMinutes,
yolo,
sandboxActive,
reason: "not-owner",
message: "OpenClaw rescue only accepts commands from an OpenClaw owner."
};
if (!input.isDirectMessage) return {
allowed: false,
enabled,
ownerDmOnly,
pendingTtlMinutes,
yolo,
sandboxActive,
reason: "not-direct-message",
message: "OpenClaw rescue is restricted to owner DMs by default."
};
return {
allowed: true,
enabled: true,
ownerDmOnly,
pendingTtlMinutes,
yolo: true,
sandboxActive: false
};
}
//#endregion
//#region src/system-agent/rescue-message.ts
const SYSTEM_AGENT_COMMAND = "/openclaw";
const RESCUE_PENDING_NAMESPACE = "rescue-pending";
const RESCUE_PENDING_MAX_ENTRIES = 1024;
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(`OpenClaw operation exited with code ${code}`);
}
},
read: () => lines.join("\n").trim()
};
}
/** Extract the command body after `/openclaw`, or null when the message is not for rescue. */
function extractSystemAgentRescueMessage(commandBody) {
const normalized = commandBody.trim();
const lower = normalized.toLowerCase();
if (lower !== SYSTEM_AGENT_COMMAND && !lower.startsWith(`${SYSTEM_AGENT_COMMAND} `)) return null;
return normalized.slice(9).trim();
}
function resolvePendingKey(input) {
const key = JSON.stringify({
accountId: resolveAccountDiscriminator(input.command),
channel: input.command.channelId ?? input.command.channel,
from: input.command.from,
senderId: input.command.senderId
});
return createHash("sha256").update(key).digest("hex").slice(0, 32);
}
function resolveAccountDiscriminator(command) {
return command.accountId?.trim() || command.to?.trim() || "default";
}
function openPendingStore(env) {
return createCorePluginStateSyncKeyedStore({
ownerId: "core:system-agent",
namespace: RESCUE_PENDING_NAMESPACE,
maxEntries: RESCUE_PENDING_MAX_ENTRIES,
overflowPolicy: "reject-new",
...env ? { env } : {}
});
}
function isPlainRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
}
function hasExactKeys(value, required, optional = []) {
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
}
function hasOptionalString(value, key) {
return !Object.hasOwn(value, key) || hasNonEmptyString(value[key]);
}
function parsePendingOperation(value) {
if (!isPlainRecord(value) || value.version !== 1 || !isPlainRecord(value.operation)) return null;
const operation = value.operation;
if (typeof operation.kind !== "string") return null;
switch (operation.kind) {
case "set-default-model":
if (!hasExactKeys(operation, ["kind", "model"]) || !hasNonEmptyString(operation.model)) return null;
break;
case "config-set":
if (!hasExactKeys(operation, [
"kind",
"path",
"value"
]) || !hasNonEmptyString(operation.path) || !hasNonEmptyString(operation.value)) return null;
break;
case "config-set-ref":
if (!hasExactKeys(operation, [
"kind",
"path",
"source",
"id"
], ["provider"]) || !hasNonEmptyString(operation.path) || operation.source !== "env" && operation.source !== "file" && operation.source !== "exec" && operation.source !== "store" || !hasNonEmptyString(operation.id) || !hasOptionalString(operation, "provider")) return null;
break;
case "setup":
if (!hasExactKeys(operation, ["kind"], ["workspace", "model"]) || !hasOptionalString(operation, "workspace") || !hasOptionalString(operation, "model")) return null;
break;
case "plugin-install":
if (!hasExactKeys(operation, ["kind", "spec"]) || !hasNonEmptyString(operation.spec)) return null;
break;
case "create-agent":
if (!hasExactKeys(operation, ["kind", "agentId"], ["workspace", "model"]) || !hasNonEmptyString(operation.agentId) || !hasOptionalString(operation, "workspace") || !hasOptionalString(operation, "model")) return null;
break;
case "gateway-start":
case "gateway-stop":
case "gateway-restart":
if (!hasExactKeys(operation, ["kind"])) return null;
break;
default: return null;
}
return isPersistentSystemAgentOperation(operation) ? operation : null;
}
function buildAuditDetails(input) {
return {
rescue: true,
channel: input.command.channelId ?? input.command.channel,
accountId: resolveAccountDiscriminator(input.command),
senderId: input.command.senderId,
from: input.command.from
};
}
function formatPersistentPlan(operation) {
return formatSystemAgentPersistentPlan(operation).replace("Say yes to apply.", "Reply /openclaw yes to apply.");
}
function formatUnsupportedRemoteOperation(operation) {
if (operation.kind === "open-tui") return ["OpenClaw 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 === "channel-setup") return ["OpenClaw rescue cannot host the interactive channel setup from a message channel.", "Run `openclaw setup` locally and say `connect " + operation.channel + "` instead."].join(" ");
if (operation.kind === "model-setup") return ["OpenClaw rescue cannot host model-provider credential setup from a message channel.", "Run `openclaw onboard` locally; it live-tests the candidate route before saving it."].join(" ");
if (operation.kind === "doctor-fix") return ["OpenClaw rescue cannot run doctor repairs from a message channel because they can change the inference route powering this session.", "On the machine running OpenClaw, with OpenClaw stopped, run `openclaw doctor --fix`."].join(" ");
if (operation.kind === "plugin-install") return ["OpenClaw rescue cannot install plugins from a message channel by default because plugin install downloads executable code.", "Use local `openclaw setup` 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 runSystemAgentRescueMessage(input) {
const rescueMessage = extractSystemAgentRescueMessage(input.commandBody);
if (rescueMessage === null) return null;
const policy = resolveSystemAgentRescuePolicy({
cfg: input.cfg,
agentId: input.agentId,
senderIsOwner: input.command.senderIsOwner,
isDirectMessage: !input.isGroup
});
if (!policy.allowed) return policy.message;
const pendingStore = openPendingStore(input.env);
const pendingKey = resolvePendingKey(input);
const approvalIntent = classifySystemAgentApprovalText(rescueMessage);
if (approvalIntent === "approve") {
const operation = parsePendingOperation(pendingStore.consume(pendingKey));
if (!operation) return "No pending OpenClaw rescue change is waiting for approval.";
const unsupported = formatUnsupportedRemoteOperation(operation);
if (unsupported) return unsupported;
const capture = createCaptureRuntime();
await executeSystemAgentOperation(operation, capture.runtime, {
approved: true,
auditDetails: buildAuditDetails(input),
deps: input.deps
});
return capture.read() || "OpenClaw rescue change applied.";
}
if (approvalIntent === "decline") return parsePendingOperation(pendingStore.consume(pendingKey)) ? "Dropped the pending OpenClaw rescue change." : "No pending OpenClaw rescue change is waiting for approval.";
pendingStore.delete(pendingKey);
const operation = parseSystemAgentOperation(rescueMessage);
const unsupported = formatUnsupportedRemoteOperation(operation);
if (unsupported) return unsupported;
if (isPersistentSystemAgentOperation(operation)) {
const nowMs = asDateTimestampMs((/* @__PURE__ */ new Date()).getTime());
const expiresAtMs = nowMs === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(policy.pendingTtlMinutes * 6e4, { nowMs });
if (nowMs === void 0 || expiresAtMs === void 0) return "OpenClaw rescue could not create a pending approval because the expiry clock is invalid.";
const ttlMs = expiresAtMs - nowMs;
pendingStore.register(pendingKey, {
version: 1,
operation
}, { ttlMs });
return formatPersistentPlan(operation);
}
const capture = createCaptureRuntime();
await executeSystemAgentOperation(operation, capture.runtime, {
approved: true,
auditDetails: buildAuditDetails(input),
deps: input.deps
});
return capture.read() || "OpenClaw listened, clicked a claw, and found nothing to change.";
}
//#endregion
export { extractSystemAgentRescueMessage, runSystemAgentRescueMessage };