UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,073 lines (1,072 loc) • 48.3 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs"; import { t as FsSafeError, w as root } from "./fs-safe-BUXBSN-4.mjs"; import { r as isMissingPathError } from "./errno-FR4CRyya.mjs"; import "./utils-DgZn2dLu.mjs"; import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs"; import "./errors-O33D01XW.mjs"; import { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs"; import { r as normalizeAgentIdStrict } from "./agent-id-D1-Q6vR4.mjs"; import { M as unregisterResolvedAgentDir, a as resolveAgentDir, d as resolveAgentWorkspaceDir, j as resolveRegisteredAgentIdForDir, k as normalizeAgentDirRegistryPath } from "./agent-scope-config-CDYpzA0r.mjs"; import "./session-key-C09ow4h0.mjs"; import { i as listAgentIds, l as tryResolveSoleAgentId } from "./agent-roster-BXD1uMJD.mjs"; import { p as resolveSessionTranscriptsDirForAgent } from "./paths-dpReqUI8.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { u as readConfigFileSnapshotForWrite } from "./io.runtime-BsUXSsD4.mjs"; import { s as withConfigMutationExclusive } from "./mutate-DJq1JylX.mjs"; import "./config-DLycWFvB.mjs"; import { a as hasGatewayClientCap, n as GATEWAY_CLIENT_IDS, t as GATEWAY_CLIENT_CAPS } from "./client-info-C8RatpKO.mjs"; import { c as resolveSharedAuthStorePath, o as resolveSharedAuthStoreOwnership } from "./path-resolve-CIRhdX6A.mjs"; import { c as readAgentDeletionJournal } from "./agent-deletion-journal-CDljXcnp.mjs"; import { g as resolveAuthProfileDatabasePath } from "./sqlite-CW8wNN4G.mjs"; import { a as validateAgentsDeleteParams, c as validateAgentsFilesSetParams, i as validateAgentsCreateParams, l as validateAgentsListParams, o as validateAgentsFilesGetParams, s as validateAgentsFilesListParams, u as validateAgentsUpdateParams } from "./src-l2dWvEsN.mjs"; import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-Dh56LniQ.mjs"; import { d as resolveOperatorRolePolicy, t as authorizeCurrentOperatorRoleScopes } from "./operator-role-policy-Bsa4SHqC.mjs"; import { i as getAgentWorkspaceAccess } from "./workspace-access-BnPMMciK.mjs"; import { l as WORKSPACE_BOOTSTRAP_FILENAMES, n as DEFAULT_BOOTSTRAP_FILENAME, r as DEFAULT_IDENTITY_FILENAME } from "./workspace-bootstrap-policy-CPdXB_so.mjs"; import { t as MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "./workspace-bootstrap-read-B5B8MlNt.mjs"; import { i as isExpectedAbsentBootstrapFile, o as isWorkspaceSetupCompleted, t as ensureAgentWorkspace } from "./workspace-BHjQqjjT.mjs"; import { l as prepareLegacyWorkspaceStateReset, u as removeLegacyWorkspaceStateForReset } from "./workspace-legacy-state-CfAX5lfc.mjs"; import { i as prepareWorkspaceStateDeletion, n as deleteWorkspaceState } from "./workspace-state-store-DCCJC3M9.mjs"; import { a as claimCompletedAgentDeletion, c as withAgentDeletion, n as AgentDeletionCommitUncertainError, t as AgentDeletionAuthorityRollbackError } from "./agent-lifecycle-registry-kGv3atS9.mjs"; import { f as withAgentExecApprovalsRemoved } from "./exec-approvals-store-DGp2moY9.mjs"; import "./exec-approvals-B1rZTeax.mjs"; import { l as sanitizeAgentIdentityLine, n as createAgentIdentityConfig, s as normalizeIdentityForFile, t as buildIdentityMarkdownForWrite } from "./identity-file-DQ9_U4uO.mjs"; import { n as resolveAgentIdentity } from "./identity-uzCeiPuC.mjs"; import "./sessions-DQtCC-5j.mjs"; import { t as purgeAgentSessionStoreEntries } from "./cleanup-service-CVriZU0p.mjs"; import { t as SessionMutationAuthorizationChangedError } from "./session-mutation-authorization-error-CCz8QGWh.mjs"; import { n as listAgentsForGateway } from "./session-utils-store-BAlWJHht.mjs"; import { n as prepareOperatorModelPresentation } from "./operator-model-presentation-tpGai_DS.mjs"; import { t as applyAgentConfig } from "./agents.config-CEaxMlw1.mjs"; import "./session-utils-seGz8XhX.mjs"; import { c as trashAllowedRoots } from "./cleanup-utils-CFLdg-fB.mjs"; import { n as createAgent } from "./agent-create-BieMRKvk.mjs"; import { r as enqueueWorkspaceFileUpdate } from "./workspace-fs-zorsuwri.mjs"; import { t as movePathToTrash } from "./browser-trash-BBLAyaRb.mjs"; import "./browser-maintenance-BbSDNJPM.mjs"; import { n as readPreparedServerMethodModelCatalogs, t as readPreparedServerMethodModelCatalog } from "./optional-model-catalog-B-pbmxbf.mjs"; import { i as isSharedAuthStoreOwner, n as formatSharedAuthStoreOwnerDeleteError, r as isInheritedAuthStoreOwner } from "./agent-delete-safety-DmMgHiSV.mjs"; import { a as prepareAgentDeleteDatabases, c as resolveSurvivingDatabaseFilePaths, i as isPathOwnedBySurvivingAgent, l as retireAgentDeleteRuntime, n as assertAgentSessionStoreDeletionSafe, o as prepareJournaledAgentDirOwnership, r as finishAgentDeleteDatabases, s as readAgentDeleteDatabaseRegistry, t as AgentSharedStoreOwnerError } from "./agent-delete-databases-nBTK8a9n.mjs"; import { a as isImplicitAgentModelUpdate, i as isConfiguredAgent, n as AgentModelSelectionError, o as updateAgentConfigEntry, r as deleteAgentConfigEntry, s as validateAgentModelSelectionUpdate, t as AgentConfigPreconditionError } from "./agents-config-mutations-ge_DxA30.mjs"; import { a as captureGatewayClientUploadCommitGuard } from "./upload-policy-B7cW3IFl.mjs"; import { t as assertValidParams } from "./validation-Bu27N0nv.mjs"; import { n as resolveConfiguredAgentIdOrRespondError } from "./agent-id-shared-D1br86TX.mjs"; import { t as reviveAgentDatabasesAfterConfigCommit } from "./server-reload-agent-databases-BbZ3naJj.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; import { resolvePathPrefixSync } from "@openclaw/fs-safe/advanced"; //#region src/gateway/server-methods/agents-files.ts const CORE_FILE_NAMES = WORKSPACE_BOOTSTRAP_FILENAMES.filter((name) => name !== DEFAULT_IDENTITY_FILENAME); const CORE_FILE_NAMES_POST_ONBOARDING = CORE_FILE_NAMES.filter((name) => name !== DEFAULT_BOOTSTRAP_FILENAME); const ALLOWED_FILE_NAMES = new Set(WORKSPACE_BOOTSTRAP_FILENAMES); function resolveAgentWorkspaceFileOrRespondError(params, respond, cfg) { const agentId = resolveConfiguredAgentIdOrRespondError(params.agentId, cfg, respond); if (!agentId) return null; const name = params.name.trim(); if (!ALLOWED_FILE_NAMES.has(name)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`)); return null; } return { agentId, workspaceDir: resolveAgentWorkspaceDir(cfg, agentId), name }; } async function statWorkspaceFileSafely(workspaceRoot, name) { try { const stat = await workspaceRoot?.stat(name); return stat?.isFile && !stat.isSymbolicLink && stat.nlink <= 1 ? { size: stat.size, updatedAtMs: Math.floor(stat.mtimeMs) } : null; } catch { return null; } } async function listAgentFiles(workspaceDir, options) { const access = getAgentWorkspaceAccess(workspaceDir); const workspaceRoot = access ? null : await root(workspaceDir).catch(() => null); const names = options?.hideBootstrap ? CORE_FILE_NAMES_POST_ONBOARDING : CORE_FILE_NAMES; return await Promise.all(names.map(async (name) => { let meta; if (access) { const stat = await access.bridge.stat({ filePath: name }); if (getAgentWorkspaceAccess(workspaceDir) !== access) throw new Error("Workspace access changed while listing Agent documents"); meta = stat?.type === "file" ? { size: stat.size, updatedAtMs: Math.floor(stat.mtimeMs) } : null; } else meta = await statWorkspaceFileSafely(workspaceRoot, name); return Object.assign({ name, path: path.join(workspaceDir, name), missing: meta === null }, meta ?? { expectedAbsent: isExpectedAbsentBootstrapFile(name) }); })); } function respondWorkspaceFileUnsafe(respond, name) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsafe workspace file "${name}"`)); } async function writeWorkspaceFileOrRespond(params) { const access = getAgentWorkspaceAccess(params.workspaceDir); if (access) { if (Buffer.byteLength(params.content) > 2097152) throw new Error("Workspace document exceeds its write bound"); params.assertCurrent?.(); await access.bridge.writeFile({ filePath: params.name, data: params.content, mkdir: false }); if (getAgentWorkspaceAccess(params.workspaceDir) !== access) throw new Error("Workspace access changed while saving Agent identity"); return true; } params.assertCurrent?.(); await fs.mkdir(params.workspaceDir, { recursive: true }); try { await (await root(params.workspaceDir)).write(params.name, params.content, { encoding: "utf8", assertBeforeMutation: params.assertCurrent }); } catch (err) { if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(params.respond, params.name); return false; } throw err; } return true; } async function readWorkspaceFileContent(workspaceDir, name) { try { const access = getAgentWorkspaceAccess(workspaceDir); if (access) { const data = await access.bridge.readFile({ filePath: name, maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES }); if (getAgentWorkspaceAccess(workspaceDir) !== access) throw new Error("Workspace access changed while reading Agent identity"); if (data.length > 2097152) throw new Error("Workspace document exceeds its read bound"); return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data); } return (await (await root(workspaceDir)).read(name, { hardlinks: "reject" })).buffer.toString("utf-8"); } catch (err) { if (isMissingPathError(err)) return; throw err; } } async function buildIdentityMarkdownOrRespondUnsafe(params) { try { return await buildIdentityMarkdownForWrite({ ...params, readWorkspaceFileContent }); } catch (err) { if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(params.respond, DEFAULT_IDENTITY_FILENAME); return null; } throw err; } } function respondWorkspaceFileMissing(params) { params.respond(true, { agentId: params.agentId, workspace: params.workspaceDir, file: { name: params.name, path: params.filePath, missing: true, expectedAbsent: isExpectedAbsentBootstrapFile(params.name) } }, void 0); } async function readWorkspaceFileHash(workspaceRoot, name) { try { const safeRead = await workspaceRoot.read(name, { hardlinks: "reject" }); return sha256Hex(safeRead.buffer); } catch (err) { if (isMissingPathError(err)) return; throw err; } } function respondWorkspaceFileConflict(respond, name, currentHash) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `agent file "${name}" changed since it was read`, { details: { type: "agent_file_conflict", name, ...currentHash ? { currentHash } : {} } })); } const agentFileHandlers = { "agents.files.list": async ({ params, respond, context }) => { if (!assertValidParams(params, validateAgentsFilesListParams, "agents.files.list", respond)) return; const cfg = context.getRuntimeConfig(); const agentId = resolveConfiguredAgentIdOrRespondError(params.agentId, cfg, respond); if (!agentId) return; const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); let hideBootstrap = false; try { hideBootstrap = await isWorkspaceSetupCompleted(workspaceDir); } catch {} respond(true, { agentId, workspace: workspaceDir, files: await listAgentFiles(workspaceDir, { hideBootstrap }) }, void 0); }, "agents.files.get": async ({ params, respond, context }) => { if (!assertValidParams(params, validateAgentsFilesGetParams, "agents.files.get", respond)) return; const resolved = resolveAgentWorkspaceFileOrRespondError(params, respond, context.getRuntimeConfig()); if (!resolved) return; const { agentId, workspaceDir, name } = resolved; const filePath = path.join(workspaceDir, name); const access = getAgentWorkspaceAccess(workspaceDir); let file; if (access) { const stat = await access.bridge.stat({ filePath: name }); if (getAgentWorkspaceAccess(workspaceDir) !== access) throw new Error("Workspace access changed while reading an Agent document"); if (!stat) { respondWorkspaceFileMissing({ respond, agentId, workspaceDir, name, filePath }); return; } const data = await access.bridge.readFile({ filePath: name, maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES }); if (getAgentWorkspaceAccess(workspaceDir) !== access || data.length > 2097152) throw new Error("Workspace document read is no longer valid"); file = { size: data.length, updatedAtMs: Math.floor(stat.mtimeMs), hash: sha256Hex(data), content: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data) }; } else { let safeRead; try { safeRead = await (await root(workspaceDir)).read(name, { hardlinks: "reject" }); } catch (err) { if (isMissingPathError(err)) { respondWorkspaceFileMissing({ respond, agentId, workspaceDir, name, filePath }); return; } if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(respond, name); return; } throw err; } file = { size: safeRead.stat.size, updatedAtMs: Math.floor(safeRead.stat.mtimeMs), hash: sha256Hex(safeRead.buffer), content: safeRead.buffer.toString("utf-8") }; } respond(true, { agentId, workspace: workspaceDir, file: { name, path: filePath, missing: false, ...file } }, void 0); }, "agents.files.set": async ({ params, respond, context }) => { if (!assertValidParams(params, validateAgentsFilesSetParams, "agents.files.set", respond)) return; const resolved = resolveAgentWorkspaceFileOrRespondError(params, respond, context.getRuntimeConfig()); if (!resolved) return; const { agentId, workspaceDir, name } = resolved; const access = getAgentWorkspaceAccess(workspaceDir); const filePath = path.join(workspaceDir, name); const content = params.content; let workspaceRoot = null; let conflict; if (access) { if (Buffer.byteLength(params.content) > 2097152) throw new Error("Workspace document exceeds its write bound"); const assertCurrent = () => { if (getAgentWorkspaceAccess(workspaceDir) !== access) throw new Error("Workspace access changed while saving an Agent document"); }; const createFileExclusive = access.bridge.createFileExclusive; if (params.expectedMissing && !createFileExclusive) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "This workspace host cannot safely create a missing Agent document. Update its workspace provider, or create the file on that host and reload it before saving.")); return; } conflict = await enqueueWorkspaceFileUpdate(async () => { assertCurrent(); if (params.expectedMissing && createFileExclusive) { const result = await createFileExclusive({ filePath: name, data: content, mkdir: true }); assertCurrent(); return result === "exists" ? { currentHash: void 0 } : void 0; } const expectedHash = params.expectedHash?.toLowerCase(); if (expectedHash) { const stat = await access.bridge.stat({ filePath: name }); assertCurrent(); let currentHash; if (stat) { const data = await access.bridge.readFile({ filePath: name, maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES }); assertCurrent(); if (data.length > 2097152) throw new Error("Workspace document exceeds its read bound"); currentHash = sha256Hex(data); } if (currentHash !== expectedHash) return { currentHash }; } await access.bridge.writeFile({ filePath: name, data: params.content, mkdir: true }); assertCurrent(); }); } else { await fs.mkdir(workspaceDir, { recursive: true }); try { workspaceRoot = await root(workspaceDir); const writeRoot = workspaceRoot; const expectedHash = params.expectedHash?.toLowerCase(); conflict = await enqueueWorkspaceFileUpdate(async () => { if (params.expectedMissing) { try { await writeRoot.create(name, content, { encoding: "utf8", atomic: true }); } catch (err) { if (err instanceof FsSafeError && err.code === "already-exists") return { currentHash: void 0 }; throw err; } return; } if (expectedHash) { const currentHash = await readWorkspaceFileHash(writeRoot, name); if (currentHash !== expectedHash) return { currentHash }; } await writeRoot.write(name, content, { encoding: "utf8" }); }); } catch (err) { if (!(err instanceof FsSafeError)) throw err; respondWorkspaceFileUnsafe(respond, name); return; } } if (conflict) { respondWorkspaceFileConflict(respond, name, conflict.currentHash); return; } const meta = access ? { size: Buffer.byteLength(content) } : await statWorkspaceFileSafely(workspaceRoot, name); respond(true, { ok: true, agentId, workspace: workspaceDir, file: { name, path: filePath, missing: false, size: meta?.size, ...!access ? { updatedAtMs: meta?.updatedAtMs } : {}, hash: sha256Hex(content), content } }, void 0); } }; //#endregion //#region src/gateway/server-methods/agents-list.ts const agentListHandler = async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateAgentsListParams, "agents.list", respond)) return; const cfg = context.getRuntimeConfig(); const agentIds = listAgentIds(cfg); const modelCatalogByAgentId = context.readPreparedGatewayModelCatalogBatch ? await readPreparedServerMethodModelCatalogs(context, agentIds) : new Map(await Promise.all(agentIds.map(async (agentId) => [agentId, await readPreparedServerMethodModelCatalog(context, { agentId })]))); const result = await listAgentsForGateway(cfg, void 0, { modelCatalogByAgentId, includeSystem: hasGatewayClientCap(client?.connect.caps, GATEWAY_CLIENT_CAPS.AGENT_KIND), httpAvatarBasePath: client?.connect.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI ? cfg.gateway?.controlUi?.basePath ?? "" : void 0 }); const currentConfig = context.getRuntimeConfig(); const roleError = authorizeCurrentOperatorRoleScopes(client, currentConfig); if (roleError) { respond(false, void 0, roleError); return; } const allowedAgents = resolveOperatorRolePolicy(client, currentConfig)?.agents; const agents = allowedAgents && allowedAgents !== "*" ? result.agents.filter((agent) => allowedAgents.includes(agent.id)) : result.agents; const policy = prepareOperatorModelPresentation({ cfg: currentConfig, policyConfig: context.getCommittedRuntimeConfig?.() ?? currentConfig, client }); respond(true, { ...result, agents: policy ? agents.map((agent) => policy.forAgent(agent.id, modelCatalogByAgentId.get(agent.id)?.entries).agent(agent)) : agents }, void 0); }; //#endregion //#region src/gateway/server-methods/agents.ts function respondAgentNotFound(respond, agentId) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" not found`)); } var AgentCleanupIdentityMismatchError = class extends Error {}; var AgentSharedAuthStoreOwnerError = class extends Error {}; function agentOwnsSharedAuthStore(cfg, agentId) { const agentDir = resolveAgentDir(cfg, agentId); return isSharedAuthStoreOwner({ ownership: resolveSharedAuthStoreOwnership(), agentAuthDbPath: resolveAuthProfileDatabasePath(agentDir), sharedAuthDbPath: resolveSharedAuthStorePath() }); } function cleanupFailure(pathname, error) { return { failed: { path: pathname, reason: (error instanceof Error && error.message ? error.message : String(error)) || "unknown error" } }; } function cleanupPathIdentity(stat) { if (typeof stat?.dev !== "number" && typeof stat?.dev !== "bigint" || typeof stat.ino !== "number" && typeof stat.ino !== "bigint") return null; const dev = Number(stat.dev); const ino = Number(stat.ino); if (!Number.isSafeInteger(dev) || !Number.isSafeInteger(ino)) throw new Error("cleanup path identity exceeds the safe integer range"); return { dev, ino }; } async function statAgentCleanupPath(cleanupPath) { const parentPath = cleanupPath.parentPath; const parentRoot = await root(parentPath, { hardlinks: "reject", symlinks: "reject" }); if (path.resolve(parentRoot.rootReal) !== parentPath) throw new FsSafeError("path-mismatch", "cleanup path parent changed before deletion"); const stat = await parentRoot.stat(path.basename(cleanupPath.trashPath)); if (stat.isSymbolicLink !== (cleanupPath.kind === "symlink")) throw new AgentCleanupIdentityMismatchError(`cleanup path changed from ${cleanupPath.kind} before deletion`); if (stat.isFile && stat.nlink > 1) throw new AgentCleanupIdentityMismatchError("hardlinked cleanup replacement preserved"); const identity = cleanupPathIdentity(stat); if (cleanupPath.preparedIdentity === null) cleanupPath.preparedIdentity = identity; else if (identity === null || identity.dev !== cleanupPath.preparedIdentity.dev || identity.ino !== cleanupPath.preparedIdentity.ino) throw new AgentCleanupIdentityMismatchError("cleanup path identity changed before deletion"); } async function removeAgentPath(cleanupPath, assertCurrent) { const pathname = cleanupPath.path; const trashPath = cleanupPath.trashPath; try { await statAgentCleanupPath(cleanupPath); } catch (error) { if (error instanceof AgentCleanupIdentityMismatchError) return { skipped: { path: pathname, reason: error.message } }; return isMissingPathError(error) ? { removed: { path: pathname, method: "missing" } } : cleanupFailure(pathname, error); } try { assertCurrent(); await movePathToTrash(trashPath, { allowedRoots: [ ...trashAllowedRoots(cleanupPath.sourcePaths, cleanupPath.kind === "symlink" ? cleanupPath.canonicalPath : void 0), os.homedir(), os.tmpdir() ] }); return { removed: { path: pathname, method: "trash" } }; } catch (error) { if (!isMissingPathError(error)) return cleanupFailure(pathname, error); try { await statAgentCleanupPath(cleanupPath); return cleanupFailure(pathname, error); } catch (statError) { return isMissingPathError(statError) ? { removed: { path: pathname, method: "missing" } } : cleanupFailure(pathname, statError); } } } async function resolveAgentDeleteCleanupTarget(pathname) { const candidate = path.resolve(pathname); try { return await fs.realpath(candidate); } catch (error) { if (!isMissingPathError(error)) throw error; const { existingPath, unresolvedSegments } = resolvePathPrefixSync(candidate); return path.resolve(existingPath, ...unresolvedSegments); } } async function prepareAgentDeleteCleanupPaths(paths, persistedPaths = []) { const uniquePaths = /* @__PURE__ */ new Map(); const addPath = (candidate) => { const existing = uniquePaths.get(candidate.trashPath); if (!existing) { uniquePaths.set(candidate.trashPath, candidate); return; } existing.sourcePaths = [.../* @__PURE__ */ new Set([...existing.sourcePaths, ...candidate.sourcePaths])]; existing.done ||= candidate.done; existing.note ??= candidate.note; existing.preparationError ??= candidate.preparationError; if (candidate.kind === "target") { existing.kind = "target"; existing.canonicalPath = candidate.canonicalPath; existing.parentPath = candidate.parentPath; existing.trashCoversDescendants ||= candidate.trashCoversDescendants; } }; for (const persistedPath of persistedPaths) { const journalPath = path.resolve(persistedPath.path); const trashPath = path.resolve(persistedPath.canonicalPath); addPath({ path: journalPath, parentPath: path.resolve(persistedPath.parentPath), canonicalPath: normalizeAgentDirRegistryPath(trashPath), trashPath, trashCoversDescendants: persistedPath.coversDescendants, kind: persistedPath.kind, preparedIdentity: persistedPath.dev === null || persistedPath.ino === null ? null : { dev: persistedPath.dev, ino: persistedPath.ino }, done: persistedPath.done, note: persistedPath.note, sourcePaths: persistedPath.sourcePaths.map((sourcePath) => path.resolve(sourcePath)) }); } for (const pathname of paths) { const sourcePath = path.resolve(pathname); let sourceParentPath = path.dirname(sourcePath); let resolvedPath = sourcePath; let preparationError; try { resolvedPath = await resolveAgentDeleteCleanupTarget(pathname); sourceParentPath = await resolveAgentDeleteCleanupTarget(path.dirname(sourcePath)); } catch (error) { preparationError = error; } let sourceStat; try { sourceStat = await fs.lstat(pathname); } catch (error) { if (!isMissingPathError(error)) preparationError ??= error; } let targetStat = sourceStat; if (resolvedPath !== sourcePath) try { targetStat = await fs.lstat(resolvedPath); } catch (error) { if (!isMissingPathError(error)) preparationError ??= error; targetStat = void 0; } const canonicalPath = normalizeAgentDirRegistryPath(resolvedPath); addPath({ path: resolvedPath, parentPath: path.dirname(resolvedPath), canonicalPath, trashPath: resolvedPath, trashCoversDescendants: targetStat ? !targetStat.isSymbolicLink() : false, kind: "target", preparedIdentity: cleanupPathIdentity(targetStat), done: false, preparationError, sourcePaths: [sourcePath] }); if (sourceStat?.isSymbolicLink() && sourcePath !== resolvedPath) addPath({ path: sourcePath, parentPath: sourceParentPath, canonicalPath, trashPath: path.join(sourceParentPath, path.basename(sourcePath)), trashCoversDescendants: false, kind: "symlink", preparedIdentity: cleanupPathIdentity(sourceStat), done: false, sourcePaths: [sourcePath] }); } const depth = (pathname) => path.relative(path.parse(pathname).root, pathname).split(path.sep).filter(Boolean).length; const cleanupDepth = (cleanupPath) => Math.max(depth(cleanupPath.canonicalPath), depth(cleanupPath.trashPath), ...cleanupPath.sourcePaths.map(depth)); const compareFallback = (left, right) => { if (left.kind !== right.kind) return left.kind === "target" ? -1 : 1; const depthDifference = cleanupDepth(right) - cleanupDepth(left); if (depthDifference !== 0) return depthDifference; return depth(right.trashPath) - depth(left.trashPath) || left.trashPath.localeCompare(right.trashPath); }; const mustPrecede = (left, right) => { if (left.kind !== right.kind) return left.kind === "target"; if (isPathInside(right.trashPath, left.trashPath)) return true; if (isPathInside(left.trashPath, right.trashPath)) return false; const rightRoots = [right.trashPath, ...right.sourcePaths]; return left.sourcePaths.some((leftSource) => rightRoots.some((rightRoot) => isPathInside(rightRoot, leftSource))); }; const remaining = [...uniquePaths.values()].toSorted(compareFallback); const ordered = []; while (remaining.length > 0) { const nextIndex = remaining.findIndex((candidate, candidateIndex) => remaining.every((other, otherIndex) => otherIndex === candidateIndex || !mustPrecede(other, candidate))); ordered.push(...remaining.splice(Math.max(0, nextIndex), 1)); } return ordered; } function cleanupPathCovers(cleanupPath, targetPath, canonicalTargetPath) { const trashTargetPath = path.resolve(targetPath); return cleanupPath.sourcePaths.includes(trashTargetPath) || cleanupPath.trashPath === trashTargetPath || cleanupPath.trashCoversDescendants && (cleanupPath.kind === "target" || isPathInside(cleanupPath.trashPath, trashTargetPath)) && isPathInside(cleanupPath.canonicalPath, canonicalTargetPath); } const agentsHandlers = { "agents.list": agentListHandler, "agents.create": async ({ params, respond, client, context }) => { if (!assertValidParams(params, validateAgentsCreateParams, "agents.create", respond)) return; try { const result = await createAgent({ name: params.name, workspace: params.workspace, model: params.model, emoji: params.emoji, avatar: params.avatar, assertIdentityInputAllowed: captureGatewayClientUploadCommitGuard({ method: "agents.create", requestParams: params, client, context }) }); if (result.status === "error") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, result.message)); return; } await reviveAgentDatabasesAfterConfigCommit([result.agentId], (message) => context.logGateway.warn(message)); respond(true, { ok: true, agentId: result.agentId, name: result.name, workspace: result.workspace, ...result.model ? { model: result.model } : {} }, void 0); } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError) { respond(false, void 0, error.error); return; } throw error; } }, "agents.update": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateAgentsUpdateParams, "agents.update", respond)) return; const assertUploadCurrent = captureGatewayClientUploadCommitGuard({ method: "agents.update", requestParams: params, client, context }); let identityPublished = false; const assertUploadAllowed = () => { if (!identityPublished) assertUploadCurrent?.(); }; const cfg = context.getRuntimeConfig(); const normalized = normalizeAgentIdStrict(params.agentId); if (!normalized.ok) { respondAgentNotFound(respond, params.agentId); return; } const agentId = normalized.value; const workspace = normalizeOptionalString(params.workspace); const workspaceDir = workspace ? resolveUserPath(workspace) : void 0; const model = params.model === null ? null : normalizeOptionalString(params.model); const name = normalizeOptionalString(params.name); const safeName = name ? sanitizeAgentIdentityLine(name) : void 0; const identity = createAgentIdentityConfig({ name: safeName, emoji: params.emoji, avatar: params.avatar }); const hasIdentityFields = Boolean(identity); const agentConfigUpdate = { agentId, ...safeName ? { name: safeName } : {}, ...workspaceDir ? { workspace: workspaceDir } : {}, ...model !== void 0 ? { model } : {}, ...params.agentRuntime ? { agentRuntime: params.agentRuntime } : {}, ...identity ? { identity } : {} }; const selectionError = validateAgentModelSelectionUpdate(params); if (selectionError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selectionError)); return; } const configured = isConfiguredAgent(cfg, agentId); if (!configured && !isImplicitAgentModelUpdate(cfg, agentConfigUpdate)) { respondAgentNotFound(respond, agentId); return; } const nextConfig = configured ? applyAgentConfig(cfg, agentConfigUpdate) : cfg; try { let ensuredWorkspace; if (workspaceDir) { const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap); ensuredWorkspace = await ensureAgentWorkspace({ dir: workspaceDir, guard: { assertHost: assertUploadAllowed }, ensureBootstrapFiles: !skipBootstrap, skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles }); } const persistedIdentity = normalizeIdentityForFile(resolveAgentIdentity(nextConfig, agentId)); if (persistedIdentity && (workspaceDir || hasIdentityFields)) { const identityWorkspaceDir = resolveAgentWorkspaceDir(nextConfig, agentId); const previousWorkspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const fallbackWorkspaceDir = workspaceDir && identityWorkspaceDir !== previousWorkspaceDir ? previousWorkspaceDir : void 0; const workspaceAccess = [identityWorkspaceDir, ...fallbackWorkspaceDir ? [fallbackWorkspaceDir] : []].map((dir) => [dir, getAgentWorkspaceAccess(dir)]); const assertWorkspaceAccessCurrent = () => { assertUploadAllowed?.(); for (const [dir, access] of workspaceAccess) if (getAgentWorkspaceAccess(dir) !== access) throw new Error("Workspace access changed while updating Agent identity"); }; const identityContent = await buildIdentityMarkdownOrRespondUnsafe({ respond, workspaceDir: identityWorkspaceDir, identity: persistedIdentity, fallbackWorkspaceDir, preferFallbackWorkspaceContent: Boolean(fallbackWorkspaceDir) && ensuredWorkspace?.identityPathCreated === true }); if (identityContent === null) return; assertWorkspaceAccessCurrent(); if (!await writeWorkspaceFileOrRespond({ respond, workspaceDir: identityWorkspaceDir, name: "IDENTITY.md", content: identityContent, assertCurrent: assertWorkspaceAccessCurrent })) return; identityPublished = true; assertWorkspaceAccessCurrent(); } await updateAgentConfigEntry({ ...agentConfigUpdate, assertCurrent: assertUploadAllowed }); } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError) { respond(false, void 0, error.error); return; } if (error instanceof AgentConfigPreconditionError) { respondAgentNotFound(respond, agentId); return; } if (error instanceof AgentModelSelectionError) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, error.message)); return; } throw error; } respond(true, { ok: true, agentId }, void 0); }, "agents.delete": async ({ params, respond, context }) => { if (!assertValidParams(params, validateAgentsDeleteParams, "agents.delete", respond)) return; const cfg = context.getRuntimeConfig(); const normalized = normalizeAgentIdStrict(params.agentId); if (!normalized.ok) { respondAgentNotFound(respond, params.agentId); return; } const agentId = normalized.value; if (agentOwnsSharedAuthStore(cfg, agentId)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, formatSharedAuthStoreOwnerDeleteError(agentId))); return; } const existingJournal = readAgentDeletionJournal(agentId); if (!isConfiguredAgent(cfg, agentId) && (!existingJournal || existingJournal.cleanupCompleted)) { respondAgentNotFound(respond, agentId); return; } if (agentId === tryResolveSoleAgentId(cfg)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Agent "${agentId}" is the only configured agent and cannot be deleted.`)); return; } if (isInheritedAuthStoreOwner(cfg, agentId)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Agent "${agentId}" owns inherited credentials through agents.defaults.authInheritance.agentId and cannot be deleted. Relocate those credentials, then re-point or remove that binding before retrying.`)); return; } const requestedDeleteFiles = params.deleteFiles ?? true; try { respond(true, await withAgentDeletion(agentId, async (begin) => withConfigMutationExclusive(async (lockedConfig) => { assertAgentSessionStoreDeletionSafe(lockedConfig, agentId); let lockedJournal = readAgentDeletionJournal(agentId); const configured = isConfiguredAgent(lockedConfig, agentId); if (agentOwnsSharedAuthStore(lockedConfig, agentId)) throw new AgentSharedAuthStoreOwnerError(formatSharedAuthStoreOwnerDeleteError(agentId)); if (!configured && (!lockedJournal || lockedJournal.cleanupCompleted)) throw new AgentConfigPreconditionError(`agent "${agentId}" not found`); if (agentId === tryResolveSoleAgentId(lockedConfig)) throw new AgentConfigPreconditionError(`agent "${agentId}" is the only configured agent`); if (isInheritedAuthStoreOwner(lockedConfig, agentId)) throw new AgentConfigPreconditionError(`agent "${agentId}" owns agents.defaults.authInheritance.agentId; relocate credentials and re-point it first`); if (configured && lockedJournal?.cleanupCompleted) { const claimed = claimCompletedAgentDeletion(agentId, lockedJournal.operationId); const remainingJournal = readAgentDeletionJournal(agentId); if (!claimed && remainingJournal) throw new Error(`agent "${agentId}" deletion tombstone changed before fresh deletion`); lockedJournal = void 0; } const deleteFiles = lockedJournal?.deleteFiles ?? requestedDeleteFiles; const deletion = begin(lockedJournal ?? { agentId, agentDir: resolveAgentDir(lockedConfig, agentId), workspaceDir: resolveAgentWorkspaceDir(lockedConfig, agentId), sessionsDir: resolveSessionTranscriptsDirForAgent(agentId), deleteFiles }); const journal = deletion.entry; let rosterCommitted = !configured; let committed; let databasePlan; try { prepareJournaledAgentDirOwnership(lockedConfig, agentId, journal.agentDir); databasePlan = await prepareAgentDeleteDatabases(lockedConfig, agentId, journal.agentDir); deletion.assertCurrent(); deletion.fenceDatabasePaths([...journal.databasePaths, ...databasePlan.fileGroups.flat()]); if (deleteFiles) { const fencedSourcePaths = new Set(journal.cleanupPaths.flatMap((cleanupPath) => cleanupPath.sourcePaths.map((sourcePath) => path.resolve(sourcePath)))); const unfencedSourcePaths = [ journal.workspaceDir, journal.agentDir, journal.sessionsDir, ...journal.databasePaths ].filter((sourcePath) => !fencedSourcePaths.has(path.resolve(sourcePath))); if (unfencedSourcePaths.length > 0) { const unfencedSourcePathSet = new Set(unfencedSourcePaths.map((sourcePath) => path.resolve(sourcePath))); const cleanupPlan = await prepareAgentDeleteCleanupPaths(unfencedSourcePaths, journal.cleanupPaths); const unresolvedPath = cleanupPlan.find((cleanupPath) => cleanupPath.preparationError !== void 0 && cleanupPath.sourcePaths.some((sourcePath) => unfencedSourcePathSet.has(path.resolve(sourcePath)))); if (unresolvedPath) throw unresolvedPath.preparationError; deletion.fenceCleanupPaths(cleanupPlan.map((cleanupPath) => { const journalPath = { path: cleanupPath.path, canonicalPath: cleanupPath.trashPath, parentPath: cleanupPath.parentPath, kind: cleanupPath.kind, sourcePaths: cleanupPath.sourcePaths, dev: cleanupPath.preparedIdentity?.dev ?? null, ino: cleanupPath.preparedIdentity?.ino ?? null, coversDescendants: cleanupPath.trashCoversDescendants, done: cleanupPath.done }; if (cleanupPath.note) journalPath.note = cleanupPath.note; return journalPath; })); } } await context.cron.removeAgentJobsTransactional(agentId, () => withAgentExecApprovalsRemoved(agentId, async () => { deletion.assertCurrent(); if (!rosterCommitted) { try { committed = await deleteAgentConfigEntry({ agentId, allowConfigSizeDrop: true, assertCurrent: deletion.assertCurrent }); } catch (error) { try { const persisted = await readConfigFileSnapshotForWrite(); if (!isConfiguredAgent(persisted.snapshot.sourceConfig, agentId)) { rosterCommitted = true; throw new AgentDeletionCommitUncertainError(error); } } catch (readError) { if (readError instanceof AgentDeletionCommitUncertainError) throw readError; throw new AgentDeletionCommitUncertainError(error); } throw error; } if (!committed.result) { rosterCommitted = !isConfiguredAgent(committed.nextConfig, agentId); const missingResultError = /* @__PURE__ */ new Error("agent delete config mutation did not return its target"); if (rosterCommitted) throw new AgentDeletionCommitUncertainError(missingResultError); throw missingResultError; } rosterCommitted = true; } })); deletion.assertCurrent(); } catch (error) { let canReleaseFence = !rosterCommitted && !lockedJournal && !(error instanceof AgentDeletionAuthorityRollbackError) && !(error instanceof AgentDeletionCommitUncertainError); if (canReleaseFence) try { const persisted = await readConfigFileSnapshotForWrite(); canReleaseFence = isConfiguredAgent(persisted.snapshot.sourceConfig, agentId); } catch { canReleaseFence = false; } if (canReleaseFence) deletion.rollback(); throw error; } await retireAgentDeleteRuntime(lockedConfig, deletion, databasePlan?.agentDirs ?? [journal.agentDir]); const deleteResult = committed?.result ?? { agentDir: journal.agentDir, workspaceDir: journal.workspaceDir, sessionsDir: journal.sessionsDir, removedBindings: 0 }; const nextConfig = committed?.nextConfig ?? lockedConfig; const agentDirRegistryPath = normalizeAgentDirRegistryPath(deleteResult.agentDir); const purgeFailed = await purgeAgentSessionStoreEntries(lockedConfig, agentId, { runDatabaseCleanup: deletion.runDatabaseCleanup }); await deletion.assertCurrentAsync(); const { closeDeletedAgentDatabases } = await import("./openclaw-agent-db-readers-CInaGeKN.mjs"); await deletion.assertCurrentAsync(); await closeDeletedAgentDatabases(agentId, databasePlan?.readerPaths ?? []); await deletion.assertCurrentAsync(); const removed = []; const failed = []; if (deleteFiles && !purgeFailed) { const survivingDatabaseFilePaths = resolveSurvivingDatabaseFilePaths(readAgentDeleteDatabaseRegistry(), agentId); const unclaimedBySurvivor = (pathname) => !isPathOwnedBySurvivingAgent(nextConfig, agentId, pathname, survivingDatabaseFilePaths); const workspaceTrashEligible = unclaimedBySurvivor(deleteResult.workspaceDir); const agentDirTrashEligible = resolveRegisteredAgentIdForDir(deleteResult.agentDir) === agentId && unclaimedBySurvivor(deleteResult.agentDir); const sessionsDirTrashEligible = unclaimedBySurvivor(deleteResult.sessionsDir); const databaseFilePaths = [...(agentDirTrashEligible ? databasePlan?.relocatedFileGroups ?? [] : databasePlan?.fileGroups ?? []).flat(), ...journal.databasePaths].filter(unclaimedBySurvivor); const eligibleSourcePaths = new Set([ ...workspaceTrashEligible ? [deleteResult.workspaceDir] : [], ...agentDirTrashEligible ? [deleteResult.agentDir] : [], ...sessionsDirTrashEligible ? [deleteResult.sessionsDir] : [], ...databaseFilePaths ].map((sourcePath) => path.resolve(sourcePath))); const cleanupPaths = (await prepareAgentDeleteCleanupPaths([], journal.cleanupPaths)).filter((cleanupPath) => cleanupPath.sourcePaths.some((sourcePath) => eligibleSourcePaths.has(sourcePath)) && (agentDirTrashEligible || !cleanupPathCovers(cleanupPath, deleteResult.agentDir, agentDirRegistryPath))); const workspaceCanonicalPath = normalizeAgentDirRegistryPath(deleteResult.workspaceDir); const workspaceCleanupPaths = cleanupPaths.filter((cleanupPath) => cleanupPathCovers(cleanupPath, deleteResult.workspaceDir, workspaceCanonicalPath)); const legacyPlan = workspaceCleanupPaths.length > 0 ? prepareLegacyWorkspaceStateReset(deleteResult.workspaceDir) : void 0; const statePlan = workspaceCleanupPaths.length > 0 ? prepareWorkspaceStateDeletion(deleteResult.workspaceDir) : void 0; const markCleanupPathDone = (cleanupPath, note) => { const canonicalPath = path.resolve(cleanupPath.trashPath); deletion.fenceCleanupPaths(journal.cleanupPaths.map((entry) => { if (path.resolve(entry.canonicalPath) !== canonicalPath || entry.kind !== cleanupPath.kind) return entry; const updated = Object.assign({}, entry, { done: true }); if (note) updated.note = note; return updated; })); cleanupPath.done = true; cleanupPath.note = note; }; const protectedCleanupPaths = []; for (const cleanupPath of cleanupPaths) { deletion.assertCurrent(); if (cleanupPath.done) { let replacementPresent = true; let note = cleanupPath.note ?? "completed cleanup path is occupied; replacement preserved"; try { await statAgentCleanupPath(cleanupPath); } catch (error) { if (isMissingPathError(error)) replacementPresent = false; else if (!(error instanceof AgentCleanupIdentityMismatchError)) note = "completed cleanup path could not be verified; replacement preserved"; } if (replacementPresent) { markCleanupPathDone(cleanupPath, note); protectedCleanupPaths.push({ cleanupPath, protectAliases: true, terminal: true, note }); } continue; } const refreshedDatabaseFilePaths = resolveSurvivingDatabaseFilePaths(readAgentDeleteDatabaseRegistry(), agentId); const blockingProtection = protectedCleanupPaths.find(({ cleanupPath: protectedPath, protectAliases }) => (cleanupPath.kind !== "symlink" || protectAliases) && (protectedPath.canonicalPath === cleanupPath.canonicalPath || isPathInside(cleanupPath.canonicalPath, protectedPath.canonicalPath)) || [protectedPath.trashPath, ...protectAliases ? protectedPath.sourcePaths : []].some((protectedSourcePath) => protectedSourcePath === cleanupPath.trashPath || isPathInside(cleanupPath.trashPath, protectedSourcePath))); const ownedBySurvivor = isPathOwnedBySurvivingAgent(nextConfig, agentId, cleanupPath.path, refreshedDatabaseFilePaths) || cleanupPathCovers(cleanupPath, deleteResult.agentDir, agentDirRegistryPath) && resolveRegisteredAgentIdForDir(deleteResult.agentDir) !== agentId; if (blockingProtection || ownedBySurvivor) { const terminal = ownedBySurvivor || blockingProtection?.terminal === true; const note = ownedBySurvivor ? "replacement owned by a surviving agent" : blockingProtection?.note; if (terminal) markCleanupPathDone(cleanupPath, note ?? "protected replacement preserved"); protectedCleanupPaths.push({ cleanupPath, protectAliases: blockingProtection?.protectAliases ?? false, terminal, note }); continue; } const outcome = cleanupPath.preparationError ? cleanupFailure(cleanupPath.path, cleanupPath.preparationError) : await removeAgentPath(cleanupPath, deletion.assertCurrent); if ("removed" in outcome) { removed.push(outcome.removed); markCleanupPathDone(cleanupPath); } else if ("skipped" in outcome) { markCleanupPathDone(cleanupPath, outcome.skipped.reason); protectedCleanupPaths.push({ cleanupPath, protectAliases: true, terminal: true, note: outcome.skipped.reason }); } else { failed.push(outcome.failed); protectedCleanupPaths.push({ cleanupPath, protectAliases: true, terminal: false }); } } if (workspaceCleanupPaths.length > 0 && workspaceCleanupPaths.every((cleanupPath) => cleanupPath.done) && legacyPlan && statePlan) try { await removeLegacyWorkspaceStateForReset(legacyPlan, { assertCurrent: deletion.assertCurrent }); deletion.assertCurrent(); await deleteWorkspaceState(statePlan, { assertCurrent: deletion.assertCurrent }); } catch {} deletion.assertCurrent(); const agentDirCleanupPaths = cleanupPaths.filter((cleanupPath) => cleanupPathCovers(cleanupPath, deleteResult.agentDir, agentDirRegistryPath)); if (agentDirCleanupPaths.length > 0 && agentDirCleanupPaths.every((cleanupPath) => cleanupPath.done)) unregisterResolvedAgentDir({ agentId, agentDir: agentDirRegistryPath }); } await finishAgentDeleteDatabases({ deletion, databasePlan, agentDir: agentDirRegistryPath, deleteFiles, complete: failed.length === 0 && !purgeFailed }); return { ok: true, agentId, removedBindings: deleteResult.removedBindings, removed, failed, ...purgeFailed ? { purgeFailed: true } : {} }; })), void 0); } catch (error) { if (error instanceof AgentSharedAuthStoreOwnerError || error instanceof AgentSharedStoreOwnerError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message)); return; } if (error instanceof AgentConfigPreconditionError) { respondAgentNotFound(respond, agentId); return; } throw error; } }, ...agentFileHandlers }; //#endregion export { agentsHandlers as t };