UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

328 lines (327 loc) 12.9 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { g as readStringValue, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { r as resolveHomeRelativePath, t as expandHomePrefix } from "./home-dir-BPhrG-aM.js"; import { At as boolean, Et as array, Nn as record, Rn as string, Xn as union, Zn as unknown, dn as literal, fn as looseObject, wn as number, yt as _enum } from "./schemas-zxit8y5H.js"; import { n as ok, t as err } from "./result-BQGgYouL.js"; import { t as DEFAULT_AGENT_ID } from "./session-key-BnWWjqNc.js"; import path from "node:path"; import { randomBytes } from "node:crypto"; //#region src/infra/exec-approvals-config.ts const toStringOrUndefined = readStringValue; const execSecuritySchema = _enum([ "allowlist", "full", "deny" ]); const execAskSchema = _enum([ "always", "off", "on-miss" ]); const persistedExecApprovalPolicySchema = looseObject({ security: execSecuritySchema.optional(), ask: execAskSchema.optional(), askFallback: execSecuritySchema.optional(), autoAllowSkills: boolean().optional() }); function normalizePersistedAllowlistSource(value) { return value === "allow-always" ? value : void 0; } const persistedExecAllowlistEntrySchema = union([string().trim().min(1), looseObject({ pattern: string().refine((value) => value.trim().length > 0), id: string().optional(), source: string().transform(normalizePersistedAllowlistSource).optional(), commandText: string().optional(), argPattern: string().optional(), lastUsedAt: number().finite().optional(), lastUsedCommand: string().optional(), lastResolvedPath: string().optional() })]).transform((value) => typeof value === "string" ? { pattern: value } : value); const persistedExecApprovalsAgentSchema = persistedExecApprovalPolicySchema.extend({ allowlist: array(persistedExecAllowlistEntrySchema).optional(), mcpTools: array(looseObject({ server: string().refine((value) => value.trim().length > 0), tool: string().refine((value) => value.trim().length > 0), source: literal("allow-always"), addedAt: number().finite().nonnegative(), lastUsedAt: number().finite().nonnegative().optional() })).optional() }); const persistedExecApprovalsAgentsSchema = unknown().refine((value) => !isRecord(value) || !Object.hasOwn(value, "__proto__")).pipe(record(string(), persistedExecApprovalsAgentSchema)); const persistedExecApprovalsSchema = looseObject({ version: literal(1), socket: looseObject({ path: string().optional(), token: string().optional() }).optional(), defaults: persistedExecApprovalPolicySchema.optional(), agents: persistedExecApprovalsAgentsSchema.optional() }); const DEFAULT_SECURITY = "full"; const DEFAULT_EXEC_APPROVAL_ASK_FALLBACK = "deny"; const DEFAULT_EXEC_APPROVALS_STATE_DIR = "~/.openclaw"; const EXEC_APPROVALS_FILE = "exec-approvals.json"; const EXEC_APPROVALS_SOCKET = "exec-approvals.sock"; function resolveExecApprovalsStateDir(env = process.env) { const override = env.OPENCLAW_STATE_DIR?.trim(); if (override) { const resolved = resolveHomeRelativePath(override, { env }); return { path: resolved, displayPath: resolved }; } return { path: expandHomePrefix(DEFAULT_EXEC_APPROVALS_STATE_DIR, { env }), displayPath: DEFAULT_EXEC_APPROVALS_STATE_DIR }; } function resolveExecApprovalsPath(env = process.env) { return path.join(resolveExecApprovalsStateDir(env).path, EXEC_APPROVALS_FILE); } function resolveExecApprovalsSocketPath() { return path.join(resolveExecApprovalsStateDir().path, EXEC_APPROVALS_SOCKET); } function resolveExecApprovalsDisplayPath() { const stateDir = resolveExecApprovalsStateDir().displayPath; const locator = path.join("state", "openclaw.sqlite#exec_approvals_config"); return stateDir === DEFAULT_EXEC_APPROVALS_STATE_DIR ? `${stateDir}/${locator}` : path.join(stateDir, locator); } function resolveExecApprovalsTranscriptPath() { return process.env.OPENCLAW_STATE_DIR?.trim() ? "$OPENCLAW_STATE_DIR/state/openclaw.sqlite#exec_approvals_config" : `${DEFAULT_EXEC_APPROVALS_STATE_DIR}/state/openclaw.sqlite#exec_approvals_config`; } function createFailClosedExecApprovalsFallback() { return normalizeExecApprovalsInternal({ version: 1, defaults: { security: "deny", ask: "off", askFallback: "deny", autoAllowSkills: false }, agents: {} }); } const diagnosticFields = /* @__PURE__ */ new Set([ "version", "socket", "path", "token", "defaults", "agents", "security", "ask", "askFallback", "autoAllowSkills", "allowlist", "mcpTools", "server", "tool", "addedAt", "pattern", "id", "source", "commandText", "argPattern", "lastUsedAt", "lastUsedCommand", "lastResolvedPath" ]); function formatPersistedExecApprovalsIssue(issue, parsed) { const fieldIssue = issue.code === "invalid_union" ? issue.errors[1]?.find((child) => child.path.length === 1) : void 0; const detail = fieldIssue ?? issue; const issuePath = fieldIssue ? [...issue.path, ...fieldIssue.path] : issue.path; let location = ""; for (const [index, segment] of issuePath.slice(0, 5).entries()) if (index === 1 && issuePath[0] === "agents") { const agents = isRecord(parsed) && isRecord(parsed.agents) ? parsed.agents : {}; const ordinal = typeof segment === "string" ? Object.keys(agents).indexOf(segment) + 1 : 0; if (!ordinal) break; location += ` entry #${ordinal}`; } else if (index === 3 && (issuePath[2] === "allowlist" || issuePath[2] === "mcpTools") && typeof segment === "number" && Number.isSafeInteger(segment) && segment >= 0) location += `[${segment}]`; else if (typeof segment === "string" && diagnosticFields.has(segment)) location += `${location ? "." : ""}${segment}`; else break; let reason = "invalid value"; switch (detail.code) { case "invalid_type": switch (detail.expected) { case "string": reason = "expected a string"; break; case "number": reason = "expected a finite number"; break; case "boolean": reason = "expected a boolean"; break; case "array": reason = "expected an array"; break; case "object": reason = "expected an object"; } break; case "invalid_value": reason = "expected a supported value"; break; case "invalid_union": reason = "expected a non-empty string or an object with a non-empty pattern"; break; case "too_small": reason = "expected a non-empty string"; break; case "custom": if (issuePath.at(-1) === "pattern") reason = "expected a non-empty string"; } return `${location || "document"}: ${reason}`; } /** Validate canonical policy and expose only a bounded, value-free failure diagnostic. */ function parsePersistedExecApprovals(raw) { let parsed; try { parsed = JSON.parse(raw); } catch { return err("invalid JSON syntax"); } try { const result = persistedExecApprovalsSchema.safeParse(parsed); if (result.success) return ok(normalizeExecApprovalsInternal(result.data)); const issue = result.error.issues[0]; return err(issue ? formatPersistedExecApprovalsIssue(issue, parsed) : "invalid approvals structure"); } catch { return err("invalid approvals structure"); } } /** Parse only structurally valid persisted approvals without inventing fallback policy. */ function tryParsePersistedExecApprovals(raw) { const result = parsePersistedExecApprovals(raw); return result.ok ? result.value : null; } function normalizeAllowlistPattern(value) { const trimmed = normalizeOptionalString(value) ?? ""; return trimmed ? normalizeLowercaseStringOrEmpty(trimmed) : null; } function mergeLegacyAgent(current, legacy) { const allowlist = []; const seen = /* @__PURE__ */ new Set(); const pushEntry = (entry) => { const patternKey = normalizeAllowlistPattern(entry.pattern); if (!patternKey) return; const key = `${patternKey}\x00${entry.argPattern?.trim() ?? ""}`; if (seen.has(key)) return; seen.add(key); allowlist.push(entry); }; for (const entry of current.allowlist ?? []) pushEntry(entry); for (const entry of legacy.allowlist ?? []) pushEntry(entry); return { ...legacy, ...current, security: current.security ?? legacy.security, ask: current.ask ?? legacy.ask, askFallback: current.askFallback ?? legacy.askFallback, autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills, allowlist: allowlist.length > 0 ? allowlist : void 0, mcpTools: current.mcpTools || legacy.mcpTools ? [...current.mcpTools ?? [], ...(legacy.mcpTools ?? []).filter((grant) => !current.mcpTools?.some((entry) => entry.server === grant.server && entry.tool === grant.tool))] : void 0 }; } function coerceAllowlistEntries(allowlist) { if (!Array.isArray(allowlist) || allowlist.length === 0) return Array.isArray(allowlist) ? allowlist : void 0; let changed = false; const result = []; for (const item of allowlist) if (typeof item === "string") { const trimmed = item.trim(); if (trimmed) { result.push({ pattern: trimmed }); changed = true; } else changed = true; } else if (item && typeof item === "object" && !Array.isArray(item)) { const pattern = item.pattern; if (typeof pattern === "string" && pattern.trim().length > 0) result.push(item); else changed = true; } else changed = true; return changed ? result.length > 0 ? result : void 0 : allowlist; } function ensureAllowlistIds(allowlist) { if (!Array.isArray(allowlist) || allowlist.length === 0) return allowlist; let changed = false; const next = allowlist.map((entry) => { if (entry.id) return entry; changed = true; return { ...entry, id: crypto.randomUUID() }; }); return changed ? next : allowlist; } function stripAllowlistCommandText(allowlist) { if (!Array.isArray(allowlist) || allowlist.length === 0) return allowlist; let changed = false; const next = allowlist.map((entry) => { if (typeof entry.commandText !== "string") return entry; changed = true; const { commandText: _commandText, ...rest } = entry; return rest; }); return changed ? next : allowlist; } function sanitizeExecApprovalPolicy(policy) { const security = toStringOrUndefined(policy?.security)?.trim(); const ask = toStringOrUndefined(policy?.ask)?.trim(); const askFallback = toStringOrUndefined(policy?.askFallback)?.trim(); return { security: security === "deny" || security === "allowlist" || security === "full" ? security : void 0, ask: ask === "off" || ask === "on-miss" || ask === "always" ? ask : void 0, askFallback: askFallback === "deny" || askFallback === "allowlist" || askFallback === "full" ? askFallback : void 0, autoAllowSkills: policy?.autoAllowSkills }; } function normalizeExecApprovalsInternal(file) { const { path: rawSocketPath, token: rawValue } = file.socket ?? {}; const socketPath = rawSocketPath?.trim(); const token = rawValue?.trim(); const agents = { ...file.agents }; const legacyDefault = agents.default; if (legacyDefault) { const main = agents[DEFAULT_AGENT_ID]; agents[DEFAULT_AGENT_ID] = main ? mergeLegacyAgent(main, legacyDefault) : legacyDefault; delete agents.default; } for (const [key, agent] of Object.entries(agents)) { const allowlist = stripAllowlistCommandText(ensureAllowlistIds(coerceAllowlistEntries(agent.allowlist))); const sanitizedPolicy = sanitizeExecApprovalPolicy(agent); if (allowlist !== agent.allowlist || sanitizedPolicy.security !== agent.security || sanitizedPolicy.ask !== agent.ask || sanitizedPolicy.askFallback !== agent.askFallback) agents[key] = { ...agent, allowlist, security: sanitizedPolicy.security, ask: sanitizedPolicy.ask, askFallback: sanitizedPolicy.askFallback }; } const sanitizedDefaults = sanitizeExecApprovalPolicy(file.defaults); return { version: 1, socket: { path: socketPath && socketPath.length > 0 ? socketPath : void 0, token: token && token.length > 0 ? token : void 0 }, defaults: { ...sanitizedDefaults }, agents }; } function mergeExecApprovalsSocketDefaults(params) { const currentSocketPath = params.current?.socket?.path?.trim(); const currentToken = params.current?.socket?.token?.trim(); const socketPath = params.normalized.socket?.path?.trim() ?? currentSocketPath ?? resolveExecApprovalsSocketPath(); const token = params.normalized.socket?.token?.trim() ?? currentToken ?? generateToken(); return { ...params.normalized, socket: { path: socketPath, token } }; } function generateToken() { return randomBytes(24).toString("base64url"); } //#endregion export { mergeExecApprovalsSocketDefaults as a, resolveExecApprovalsDisplayPath as c, resolveExecApprovalsTranscriptPath as d, tryParsePersistedExecApprovals as f, generateToken as i, resolveExecApprovalsPath as l, DEFAULT_SECURITY as n, normalizeExecApprovalsInternal as o, createFailClosedExecApprovalsFallback as r, parsePersistedExecApprovals as s, DEFAULT_EXEC_APPROVAL_ASK_FALLBACK as t, resolveExecApprovalsSocketPath as u };