UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

922 lines (921 loc) 41.5 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { l as readConfigFileSnapshotForWrite } from "./io.runtime-B9iJRs3w.js"; import { r as isMissingPathError } from "./errno-CkbDOfLk.js"; import { t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js"; import { r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import "./utils-P__uGsPB.js"; import { r as normalizeAgentIdStrict } from "./agent-id-CeT3w4ap.js"; import { O as tryResolveSoleAgentId, m as resolveAgentWorkspaceDir, o as listAgentIds, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import "./session-key-BnWWjqNc.js"; import { a as unregisterResolvedAgentDir, i as resolveRegisteredAgentIdForDir, n as normalizeAgentDirRegistryPath, r as registerResolvedAgentDir } from "./agent-dir-registry-Dkh28921.js"; import "./errors-Db3Ymjlb.js"; import { l as resolveSessionTranscriptsDirForAgent } from "./paths-CXdaYWF_.js"; import "./agent-scope-DbtJyKUL.js"; import { a as hasGatewayClientCap, n as GATEWAY_CLIENT_IDS, t as GATEWAY_CLIENT_CAPS } from "./client-info-B1bPgeKr.js"; import { o as withConfigMutationExclusive } from "./mutate-ZNN4iFCn.js"; import "./config-Cs0XXL3x.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { _g as formatValidationErrors, a as validateAgentsDeleteParams, c as validateAgentsFilesSetParams, i as validateAgentsCreateParams, l as validateAgentsListParams, o as validateAgentsFilesGetParams, s as validateAgentsFilesListParams, u as validateAgentsUpdateParams } from "./src-BiL5aQto.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { o as resolveSharedAuthStoreOwnership, s as resolveSharedAuthStorePath } from "./path-resolve-oRkRBkQd.js"; import { _ as readAgentDeletionJournal } from "./openclaw-agent-db-lease-Djvd6LWN.js"; import { f as unregisterOpenClawAgentDatabase } from "./openclaw-agent-db-maintenance-wTIy-jt-.js"; import { h as resolveAuthProfileDatabasePath } from "./sqlite-MN_7y26V.js"; import { b as prepareLegacyWorkspaceStateReset, i as deleteWorkspaceState, s as prepareWorkspaceStateDeletion, x as removeLegacyWorkspaceStateForReset } from "./workspace-state-store-DdpcP8H6.js"; import { _ as isWorkspaceSetupCompleted, h as isExpectedAbsentBootstrapFile, l as WORKSPACE_BOOTSTRAP_FILENAMES, n as DEFAULT_BOOTSTRAP_FILENAME, p as ensureAgentWorkspace, r as DEFAULT_IDENTITY_FILENAME } from "./workspace-ConDEamr.js"; import "./sessions-9nxpeTwt.js"; import { t as purgeAgentSessionStoreEntries } from "./cleanup-service-DoQBUGSQ.js"; import { n as resolveAgentIdentity } from "./identity-L3YWE18q.js"; import "./exec-approvals-BSZ-fPIY.js"; import { a as claimCompletedAgentDeletion, n as AgentDeletionCommitUncertainError, r as beginAgentDeletion, t as AgentDeletionAuthorityRollbackError } from "./agent-lifecycle-registry-WgCc3vx1.js"; import { p as withAgentExecApprovalsRemoved } from "./exec-approvals-store-DBR0neS0.js"; import { a as normalizeIdentityForFile, i as mergeIdentityMarkdownContent, o as sanitizeAgentIdentityLine, t as createAgentIdentityConfig } from "./identity-file-Dnl4bkpm.js"; import { n as listAgentsForGateway } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { t as movePathToTrash } from "./browser-trash-CkBSP7Bh.js"; import "./browser-maintenance-DQ13tIiN.js"; import { t as applyAgentConfig } from "./agents.config-CFXmxe4B.js"; import { n as createAgent } from "./agent-create-DMopBnuA.js"; import { i as isSharedAuthStoreOwner, n as formatSharedAuthStoreOwnerDeleteError, r as isInheritedAuthStoreOwner } from "./agent-delete-safety-CIoplT3h.js"; import { i as resolveSurvivingDatabaseFilePaths, n as prepareAgentDeleteDatabases, r as readAgentDeleteDatabaseRegistry, t as isPathOwnedBySurvivingAgent } from "./agent-delete-databases-Kbifmfvu.js"; import { i as updateAgentConfigEntry, n as deleteAgentConfigEntry, r as isConfiguredAgent, t as AgentConfigPreconditionError } from "./agents-config-mutations-B1KH7MZ-.js"; import { t as readPreparedServerMethodModelCatalog } from "./optional-model-catalog-CIJvOLOk.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/gateway/server-methods/agents.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 agentsHandlerDeps = { root, isWorkspaceSetupCompleted }; const ALLOWED_FILE_NAMES = new Set(WORKSPACE_BOOTSTRAP_FILENAMES); function resolveAgentWorkspaceFileOrRespondError(params, respond, cfg) { const rawAgentId = params.agentId; const agentId = resolveAgentIdOrError(typeof rawAgentId === "string" || typeof rawAgentId === "number" ? String(rawAgentId) : "", cfg); if (!agentId) { respondAgentNotFound(respond, String(rawAgentId)); return null; } const rawName = params.name; const name = (typeof rawName === "string" || typeof rawName === "number" ? String(rawName) : "").trim(); if (!ALLOWED_FILE_NAMES.has(name)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`)); return null; } return { cfg, agentId, workspaceDir: resolveAgentWorkspaceDir(cfg, agentId), name }; } function isRegularWorkspaceFileStat(stat) { const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile; const isSymbolicLink = typeof stat.isSymbolicLink === "function" ? stat.isSymbolicLink() : stat.isSymbolicLink; return isFile && !isSymbolicLink && stat.nlink <= 1; } function toWorkspaceFileMeta(stat) { if (!isRegularWorkspaceFileStat(stat)) return null; return { size: stat.size, updatedAtMs: Math.floor(stat.mtimeMs) }; } async function statWorkspaceFileSafely(workspaceRoot, workspaceDir, name) { try { return toWorkspaceFileMeta(workspaceRoot ? await workspaceRoot.stat(name) : await fs.lstat(path.join(workspaceDir, name))); } catch { if (!workspaceRoot) return null; try { return toWorkspaceFileMeta(await fs.lstat(path.join(workspaceDir, name))); } catch { return null; } } } async function openWorkspaceRootSafely(workspaceDir) { try { return await agentsHandlerDeps.root(workspaceDir); } catch { return null; } } async function listAgentFiles(workspaceDir, options) { const files = []; const workspaceRoot = await openWorkspaceRootSafely(workspaceDir); if (!workspaceRoot) return (options?.hideBootstrap ? CORE_FILE_NAMES_POST_ONBOARDING : CORE_FILE_NAMES).map((name) => ({ name, path: path.join(workspaceDir, name), missing: true, expectedAbsent: isExpectedAbsentBootstrapFile(name) })); const coreFileNames = options?.hideBootstrap ? CORE_FILE_NAMES_POST_ONBOARDING : CORE_FILE_NAMES; for (const name of coreFileNames) { const filePath = path.join(workspaceDir, name); const meta = await statWorkspaceFileSafely(workspaceRoot, workspaceDir, name); if (meta) files.push({ name, path: filePath, missing: false, size: meta.size, updatedAtMs: meta.updatedAtMs }); else files.push({ name, path: filePath, missing: true, expectedAbsent: isExpectedAbsentBootstrapFile(name) }); } return files; } function resolveAgentIdOrError(agentIdRaw, cfg) { const normalized = normalizeAgentIdStrict(agentIdRaw); if (!normalized.ok) return null; const agentId = normalized.value; if (!new Set(listAgentIds(cfg)).has(agentId)) return null; return agentId; } function respondInvalidMethodParams(respond, method, errors) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid ${method} params: ${formatValidationErrors(errors)}`)); } 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 agentsHandlerDeps.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) { 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 { await movePathToTrash(trashPath); 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) { let candidate = path.resolve(pathname); const missingSuffix = []; while (true) try { return path.resolve(await fs.realpath(candidate), ...missingSuffix); } catch (error) { if (!isMissingPathError(error)) throw error; let candidateStat; try { candidateStat = await fs.lstat(candidate); } catch (statError) { if (!isMissingPathError(statError)) throw statError; } if (candidateStat?.isSymbolicLink()) { const linkTarget = await fs.readlink(candidate); const resolvedLinkTarget = await resolveAgentDeleteCleanupTarget(path.isAbsolute(linkTarget) ? linkTarget : path.resolve(path.dirname(candidate), linkTarget)); return path.resolve(resolvedLinkTarget, ...missingSuffix); } const parent = path.dirname(candidate); if (parent === candidate) throw error; missingSuffix.unshift(path.basename(candidate)); candidate = parent; } } 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; } }; if (persistedPaths.length > 0) 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); let trashCoversDescendants = false; if (targetStat) trashCoversDescendants = !targetStat.isSymbolicLink(); addPath({ path: resolvedPath, parentPath: path.dirname(resolvedPath), canonicalPath, trashPath: resolvedPath, trashCoversDescendants, 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); } function unregisterAgentDeleteDatabases(agentId, databasePaths) { for (const databasePath of databasePaths) unregisterOpenClawAgentDatabase({ agentId, path: databasePath }); } function prepareJournaledAgentDirOwnership(cfg, agentId, agentDir) { for (const configuredAgentId of listAgentIds(cfg)) resolveAgentDir(cfg, configuredAgentId); if (resolveRegisteredAgentIdForDir(agentDir) !== void 0) return; registerResolvedAgentDir({ agentId, agentDir }); } function respondWorkspaceFileUnsafe(respond, name) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsafe workspace file "${name}"`)); } 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 writeWorkspaceFileOrRespond(params) { await fs.mkdir(params.workspaceDir, { recursive: true }); try { await (await agentsHandlerDeps.root(params.workspaceDir)).write(params.name, params.content, { encoding: "utf8" }); } catch (err) { if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(params.respond, params.name); return false; } throw err; } return true; } async function readWorkspaceFileContent(workspaceDir, name) { try { return (await (await agentsHandlerDeps.root(workspaceDir)).read(name, { hardlinks: "reject", nonBlockingRead: true })).buffer.toString("utf-8"); } catch (err) { if (isMissingPathError(err)) return; throw err; } } async function buildIdentityMarkdownForWrite(params) { let baseContent; if (params.preferFallbackWorkspaceContent && params.fallbackWorkspaceDir) { baseContent = await readWorkspaceFileContent(params.fallbackWorkspaceDir, DEFAULT_IDENTITY_FILENAME); if (baseContent === void 0) baseContent = await readWorkspaceFileContent(params.workspaceDir, DEFAULT_IDENTITY_FILENAME); } else { baseContent = await readWorkspaceFileContent(params.workspaceDir, DEFAULT_IDENTITY_FILENAME); if (baseContent === void 0 && params.fallbackWorkspaceDir) baseContent = await readWorkspaceFileContent(params.fallbackWorkspaceDir, DEFAULT_IDENTITY_FILENAME); } return mergeIdentityMarkdownContent(baseContent, params.identity); } async function buildIdentityMarkdownOrRespondUnsafe(params) { try { return await buildIdentityMarkdownForWrite(params); } catch (err) { if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(params.respond, DEFAULT_IDENTITY_FILENAME); return null; } throw err; } } const agentsHandlers = { "agents.list": async ({ params, respond, context, client }) => { if (!validateAgentsListParams(params)) { respondInvalidMethodParams(respond, "agents.list", validateAgentsListParams.errors); return; } const cfg = context.getRuntimeConfig(); const modelCatalogByAgentId = new Map(await Promise.all(listAgentIds(cfg).map(async (agentId) => [agentId, await readPreparedServerMethodModelCatalog(context, { agentId })]))); respond(true, 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 }), void 0); }, "agents.create": async ({ params, respond }) => { if (!validateAgentsCreateParams(params)) { respondInvalidMethodParams(respond, "agents.create", validateAgentsCreateParams.errors); return; } const result = await createAgent({ name: params.name, workspace: params.workspace, model: params.model, emoji: params.emoji, avatar: params.avatar }); if (result.status === "error") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, result.message)); return; } respond(true, { ok: true, agentId: result.agentId, name: result.name, workspace: result.workspace, ...result.model ? { model: result.model } : {} }, void 0); }, "agents.update": async ({ params, respond, context }) => { if (!validateAgentsUpdateParams(params)) { respondInvalidMethodParams(respond, "agents.update", validateAgentsUpdateParams.errors); return; } const cfg = context.getRuntimeConfig(); const normalized = normalizeAgentIdStrict(params.agentId); if (!normalized.ok) { respondAgentNotFound(respond, params.agentId); return; } const agentId = normalized.value; if (!isConfiguredAgent(cfg, agentId)) { respondAgentNotFound(respond, agentId); return; } const workspaceDir = typeof params.workspace === "string" && params.workspace.trim() ? resolveUserPath(params.workspace.trim()) : void 0; const model = params.model === null ? null : normalizeOptionalString(params.model); const safeName = typeof params.name === "string" && params.name.trim() ? sanitizeAgentIdentityLine(params.name.trim()) : 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 } : {}, ...identity ? { identity } : {} }; const nextConfig = applyAgentConfig(cfg, agentConfigUpdate); let ensuredWorkspace; if (workspaceDir) { const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap); ensuredWorkspace = await ensureAgentWorkspace({ dir: workspaceDir, 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 identityContent = await buildIdentityMarkdownOrRespondUnsafe({ respond, workspaceDir: identityWorkspaceDir, identity: persistedIdentity, fallbackWorkspaceDir, preferFallbackWorkspaceContent: Boolean(fallbackWorkspaceDir) && ensuredWorkspace?.identityPathCreated === true }); if (identityContent === null) return; if (!await writeWorkspaceFileOrRespond({ respond, workspaceDir: identityWorkspaceDir, name: "IDENTITY.md", content: identityContent })) return; } try { await updateAgentConfigEntry(agentConfigUpdate); } catch (error) { if (error instanceof AgentConfigPreconditionError) { respondAgentNotFound(respond, agentId); return; } throw error; } respond(true, { ok: true, agentId }, void 0); }, "agents.delete": async ({ params, respond, context }) => { if (!validateAgentsDeleteParams(params)) { respondInvalidMethodParams(respond, "agents.delete", validateAgentsDeleteParams.errors); 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 = typeof params.deleteFiles === "boolean" ? params.deleteFiles : true; try { respond(true, await withConfigMutationExclusive(async (lockedConfig) => { 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 = beginAgentDeletion(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 = prepareAgentDeleteDatabases(lockedConfig, agentId, journal.agentDir); 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(({ path: cleanupPath, trashPath, parentPath, kind, preparedIdentity, trashCoversDescendants, done, note, sourcePaths }) => { const journalPath = { path: cleanupPath, canonicalPath: trashPath, parentPath, kind, sourcePaths, dev: preparedIdentity?.dev ?? null, ino: preparedIdentity?.ino ?? null, coversDescendants: trashCoversDescendants, done }; if (note) journalPath.note = note; return journalPath; })); } } await context.cron.removeAgentJobsTransactional(agentId, async () => await withAgentExecApprovalsRemoved(agentId, async () => { if (!rosterCommitted) { try { committed = await deleteAgentConfigEntry({ agentId }); } 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; } })); } 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; } 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 }); const removed = []; const failed = []; if (deleteFiles && !purgeFailed) { const survivingDatabaseFilePaths = resolveSurvivingDatabaseFilePaths(readAgentDeleteDatabaseRegistry(), agentId); const workspaceTrashEligible = !isPathOwnedBySurvivingAgent(nextConfig, agentId, deleteResult.workspaceDir, survivingDatabaseFilePaths); const agentDirTrashEligible = resolveRegisteredAgentIdForDir(deleteResult.agentDir) === agentId && !isPathOwnedBySurvivingAgent(nextConfig, agentId, deleteResult.agentDir, survivingDatabaseFilePaths); const sessionsDirTrashEligible = !isPathOwnedBySurvivingAgent(nextConfig, agentId, deleteResult.sessionsDir, survivingDatabaseFilePaths); const databaseFilePaths = [...(agentDirTrashEligible ? databasePlan?.relocatedFileGroups ?? [] : databasePlan?.fileGroups ?? []).flat(), ...journal.databasePaths].filter((pathname) => !isPathOwnedBySurvivingAgent(nextConfig, agentId, pathname, survivingDatabaseFilePaths)); 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 outcomes = []; const completedCleanupPaths = new Set(cleanupPaths.filter((cleanupPath) => cleanupPath.done)); 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; completedCleanupPaths.add(cleanupPath); }; const protectedCleanupPaths = []; for (const cleanupPath of cleanupPaths) { 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); outcomes.push({ cleanupPath, outcome }); if ("removed" in outcome) markCleanupPathDone(cleanupPath); else if ("skipped" in outcome) { markCleanupPathDone(cleanupPath, outcome.skipped.reason); protectedCleanupPaths.push({ cleanupPath, protectAliases: true, terminal: true, note: outcome.skipped.reason }); } else protectedCleanupPaths.push({ cleanupPath, protectAliases: true, terminal: false }); } for (const { outcome } of outcomes) if ("removed" in outcome) removed.push(outcome.removed); else if ("failed" in outcome) failed.push(outcome.failed); if (workspaceCleanupPaths.length > 0 && workspaceCleanupPaths.every((cleanupPath) => completedCleanupPaths.has(cleanupPath)) && legacyPlan && statePlan) try { await removeLegacyWorkspaceStateForReset(legacyPlan); deleteWorkspaceState(statePlan); } catch {} const agentDirCleanupPaths = cleanupPaths.filter((cleanupPath) => cleanupPathCovers(cleanupPath, deleteResult.agentDir, agentDirRegistryPath)); if (agentDirCleanupPaths.length > 0 && agentDirCleanupPaths.every((cleanupPath) => completedCleanupPaths.has(cleanupPath))) unregisterResolvedAgentDir({ agentId, agentDir: agentDirRegistryPath }); } if (failed.length === 0 && !purgeFailed) { unregisterResolvedAgentDir({ agentId, agentDir: agentDirRegistryPath }); if (deleteFiles) unregisterAgentDeleteDatabases(agentId, databasePlan?.registrationPaths ?? []); deletion.finish(); } return { ok: true, agentId, removedBindings: deleteResult.removedBindings, removed, failed, ...purgeFailed ? { purgeFailed: true } : {} }; }), void 0); } catch (error) { if (error instanceof AgentSharedAuthStoreOwnerError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message)); return; } if (error instanceof AgentConfigPreconditionError) { respondAgentNotFound(respond, agentId); return; } throw error; } }, "agents.files.list": async ({ params, respond, context }) => { if (!validateAgentsFilesListParams(params)) { respondInvalidMethodParams(respond, "agents.files.list", validateAgentsFilesListParams.errors); return; } const cfg = context.getRuntimeConfig(); const agentId = resolveAgentIdOrError(params.agentId, cfg); if (!agentId) { respondAgentNotFound(respond, params.agentId); return; } const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); let hideBootstrap = false; try { hideBootstrap = await agentsHandlerDeps.isWorkspaceSetupCompleted(workspaceDir); } catch {} respond(true, { agentId, workspace: workspaceDir, files: await listAgentFiles(workspaceDir, { hideBootstrap }) }, void 0); }, "agents.files.get": async ({ params, respond, context }) => { if (!validateAgentsFilesGetParams(params)) { respondInvalidMethodParams(respond, "agents.files.get", validateAgentsFilesGetParams.errors); return; } const resolved = resolveAgentWorkspaceFileOrRespondError(params, respond, context.getRuntimeConfig()); if (!resolved) return; const { agentId, workspaceDir, name } = resolved; const filePath = path.join(workspaceDir, name); let safeRead; try { safeRead = await (await agentsHandlerDeps.root(workspaceDir)).read(name, { hardlinks: "reject", nonBlockingRead: true }); } catch (err) { if (isMissingPathError(err)) { respondWorkspaceFileMissing({ respond, agentId, workspaceDir, name, filePath }); return; } if (err instanceof FsSafeError) { respondWorkspaceFileUnsafe(respond, name); return; } throw err; } respond(true, { agentId, workspace: workspaceDir, file: { name, path: filePath, missing: false, size: safeRead.stat.size, updatedAtMs: Math.floor(safeRead.stat.mtimeMs), content: safeRead.buffer.toString("utf-8") } }, void 0); }, "agents.files.set": async ({ params, respond, context }) => { if (!validateAgentsFilesSetParams(params)) { respondInvalidMethodParams(respond, "agents.files.set", validateAgentsFilesSetParams.errors); return; } const resolved = resolveAgentWorkspaceFileOrRespondError(params, respond, context.getRuntimeConfig()); if (!resolved) return; const { agentId, workspaceDir, name } = resolved; await fs.mkdir(workspaceDir, { recursive: true }); const filePath = path.join(workspaceDir, name); const content = params.content; let workspaceRoot; try { workspaceRoot = await agentsHandlerDeps.root(workspaceDir); await workspaceRoot.write(name, content, { encoding: "utf8" }); } catch (err) { if (!(err instanceof FsSafeError)) throw err; respondWorkspaceFileUnsafe(respond, name); return; } const meta = await statWorkspaceFileSafely(workspaceRoot, workspaceDir, name); respond(true, { ok: true, agentId, workspace: workspaceDir, file: { name, path: filePath, missing: false, size: meta?.size, updatedAtMs: meta?.updatedAtMs, content } }, void 0); } }; //#endregion export { agentsHandlers as t };