UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

366 lines (365 loc) • 15.9 kB
import { c as normalizeOptionalLowercaseString, g as readStringValue } from "./string-coerce-1VuiLaaR.mjs"; import { n as resolveHomeRelativePath, t as expandHomePrefix } from "./home-dir-Dc6nN6HB.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { An as never, Et as _enum, Jn as string, Lt as boolean, Nt as array, Pn as number, Sn as looseObject, Un as record, or as union, sr as unknown, xn as literal } from "./schemas-BOYIvvln.mjs"; import { n as formatDoctorStateRepairFailure } from "./state-repair-message-B5Bu99oR.mjs"; import { n as ok, t as err } from "./result-BQGgYouL.mjs"; import { t as DEFAULT_AGENT_ID } from "./session-key-C09ow4h0.mjs"; import { t as pathMayExistSync } from "./path-existence-O1bMV6vh.mjs"; import path from "node:path"; import { randomBytes } from "node:crypto"; //#region src/infra/exec-approvals-config.ts 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 = looseObject({ pattern: string().refine((value) => value.trim().length > 0), id: string().min(1), source: literal("allow-always").optional(), commandText: never().optional(), argPattern: string().optional(), lastUsedAt: number().finite().optional(), lastUsedCommand: string().optional(), lastResolvedPath: string().optional() }); const legacyExecAllowlistEntrySchema = union([string().trim().min(1), persistedExecAllowlistEntrySchema.extend({ id: string().optional(), source: string().transform(normalizePersistedAllowlistSource).optional(), commandText: string().optional() })]).transform((value) => typeof value === "string" ? { pattern: value } : value); function createExecApprovalsSchema(allowlistEntry, legacy) { const agentSchema = persistedExecApprovalPolicySchema.extend({ allowlist: array(allowlistEntry).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 agentsSchema = unknown().refine((value) => !isRecord(value) || !Object.hasOwn(value, "__proto__")).pipe(record(string(), agentSchema)).refine((value) => legacy || !Object.hasOwn(value, "default")); return looseObject({ version: literal(1), socket: looseObject({ path: string().optional(), token: string().optional() }).optional(), defaults: persistedExecApprovalPolicySchema.optional(), agents: agentsSchema.optional() }); } const persistedExecApprovalsSchema = createExecApprovalsSchema(persistedExecAllowlistEntrySchema, false); const legacyExecApprovalsSchema = createExecApprovalsSchema(legacyExecAllowlistEntrySchema, true); const LEGACY_EXEC_APPROVALS_DIAGNOSTIC = "Legacy exec approval policy requires Doctor repair"; 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(env = process.env) { const stateDir = resolveExecApprovalsStateDir(env).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 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" ]); const diagnosticTypeReasons = /* @__PURE__ */ new Map([ ["string", "expected a string"], ["number", "expected a finite number"], ["boolean", "expected a boolean"], ["array", "expected an array"], ["object", "expected an object"] ]); 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": reason = diagnosticTypeReasons.get(detail.expected) ?? reason; 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 parseExecApprovals(raw, schema) { let parsed; try { parsed = JSON.parse(raw); } catch { return err("invalid JSON syntax"); } try { const result = schema.safeParse(parsed); if (result.success) return ok(result.data); if (schema === persistedExecApprovalsSchema && legacyExecApprovalsSchema.safeParse(parsed).success) return err(LEGACY_EXEC_APPROVALS_DIAGNOSTIC); const issue = result.error.issues[0]; return err(issue ? formatPersistedExecApprovalsIssue(issue, parsed) : "invalid approvals structure"); } catch { return err("invalid approvals structure"); } } function parsePersistedExecApprovals(raw) { return parseExecApprovals(raw, persistedExecApprovalsSchema); } /** Doctor imports historical documents before ordinary readers accept their canonical shape. */ function parseLegacyExecApprovals(raw) { const parsed = parseExecApprovals(raw, legacyExecApprovalsSchema); return parsed.ok ? ok(normalizeExecApprovalsInternal(parsed.value)) : parsed; } /** Parse only structurally valid persisted approvals without inventing fallback policy. */ function tryParsePersistedExecApprovals(raw) { const result = parsePersistedExecApprovals(raw); return result.ok ? result.value : null; } function mergeLegacyAgent(current, legacy) { const allowlist = []; const seen = /* @__PURE__ */ new Set(); const pushEntry = (entry) => { const patternKey = normalizeOptionalLowercaseString(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(); changed = true; if (trimmed) result.push({ pattern: trimmed }); } 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 normalizeAllowlistMetadata(allowlist) { if (!Array.isArray(allowlist) || allowlist.length === 0) return allowlist; let changed = false; const next = allowlist.map((entry) => { let normalized = entry; const source = normalizePersistedAllowlistSource(normalized.source ?? ""); if (normalized.source !== source) normalized = { ...normalized, source }; if (!normalized.id) normalized = { ...normalized, id: crypto.randomUUID() }; if (typeof normalized.commandText === "string") { const { commandText: _commandText, ...rest } = normalized; normalized = rest; } changed ||= normalized !== entry; return normalized; }); return changed ? next : allowlist; } function sanitizeExecApprovalPolicy(policy) { const security = readStringValue(policy?.security)?.trim(); const ask = readStringValue(policy?.ask)?.trim(); const askFallback = readStringValue(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 = normalizeAllowlistMetadata(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 //#region src/infra/exec-approvals-migration-gate.ts const DOCTOR_CLAIM_SUFFIX = ".doctor-importing"; const legacyAbsenceCache = /* @__PURE__ */ new Set(); /** * Doctor repairs whichever state directory its own environment resolves to, so a bare * `openclaw doctor --fix` repairs the default root while a scoped install stays blocked. * Name the directory whenever this process is scoped to a non-default one. Say it in * prose rather than as a `VAR=value cmd` one-liner, which no Windows shell accepts. */ function doctorFixInstruction(filePath, env) { const command = "Run `openclaw doctor --fix`"; if (filePath.endsWith("#exec_approvals_config")) return `${command} with OPENCLAW_STATE_DIR set to ${path.dirname(path.dirname(filePath))}`; return env.OPENCLAW_STATE_DIR?.trim() ? `${command} with OPENCLAW_STATE_DIR set to ${path.dirname(filePath)}` : command; } var ExecApprovalsMigrationRequiredError = class extends Error { constructor(filePath, operation, problem = "Legacy exec approvals exist", env = process.env) { super(operation === "doctor" ? formatDoctorStateRepairFailure(`${problem} at ${filePath}`, "Stop the Gateway and node hosts, then reconcile this file with a verified copy of the intended exec policy; preserve existing SQLite policy.") : `${problem} at ${filePath}. ${doctorFixInstruction(filePath, env)} before using exec approvals.`); this.name = "ExecApprovalsMigrationRequiredError"; } }; /** Refuse runtime access until Doctor owns the one-time legacy import. */ function assertNoPendingLegacyExecApprovals(options = {}) { const sourcePath = resolveExecApprovalsPath(options.env); if (legacyAbsenceCache.has(sourcePath)) return; const probe = options.pathMayExist ?? pathMayExistSync; const sourceBefore = probe(sourcePath); const claim = probe(`${sourcePath}${DOCTOR_CLAIM_SUFFIX}`); const sourceAfter = probe(sourcePath); if (sourceBefore || claim || sourceAfter) throw new ExecApprovalsMigrationRequiredError(options.operation === "doctor" && !sourceBefore && !sourceAfter ? `${sourcePath}${DOCTOR_CLAIM_SUFFIX}` : sourcePath, options.operation, void 0, options.env); legacyAbsenceCache.add(sourcePath); } function resetExecApprovalsMigrationGateForTest() { legacyAbsenceCache.clear(); } //#endregion export { DEFAULT_SECURITY as a, generateToken as c, parseLegacyExecApprovals as d, parsePersistedExecApprovals as f, tryParsePersistedExecApprovals as g, resolveExecApprovalsSocketPath as h, DEFAULT_EXEC_APPROVAL_ASK_FALLBACK as i, mergeExecApprovalsSocketDefaults as l, resolveExecApprovalsPath as m, assertNoPendingLegacyExecApprovals as n, LEGACY_EXEC_APPROVALS_DIAGNOSTIC as o, resolveExecApprovalsDisplayPath as p, resetExecApprovalsMigrationGateForTest as r, createFailClosedExecApprovalsFallback as s, ExecApprovalsMigrationRequiredError as t, normalizeExecApprovalsInternal as u };