openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
294 lines (293 loc) • 15.3 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js";
import "./utils-P__uGsPB.js";
import { n as normalizeAgentId, r as normalizeAgentIdStrict } from "./agent-id-CeT3w4ap.js";
import { S as toAgentEntriesRecord, i as listAgentEntries, m as resolveAgentWorkspaceDir, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { l as resolveSessionTranscriptsDirForAgent } from "./paths-CXdaYWF_.js";
import "./agent-scope-DbtJyKUL.js";
import { t as ConfigMutationConflictError } from "./mutation-conflict-Be0wSyDG.js";
import { r as hasResolvedRosterBeforeMigrations } from "./agent-roster-provenance-TGrT9xta.js";
import { a as transformConfigFileWithRetry, o as withConfigMutationExclusive } from "./mutate-ZNN4iFCn.js";
import "./config-Cs0XXL3x.js";
import { o as resolveSharedAuthStoreOwnership } from "./path-resolve-oRkRBkQd.js";
import { _ as readAgentDeletionJournal } from "./openclaw-agent-db-lease-Djvd6LWN.js";
import { a as recordAgentProvenance } from "./agent-provenance-Cp8HmAX0.js";
import { p as ensureAgentWorkspace, r as DEFAULT_IDENTITY_FILENAME } from "./workspace-ConDEamr.js";
import { r as isReservedSystemAgentId } from "./agent-id-D7twbNRG.js";
import { a as claimCompletedAgentDeletion } from "./agent-lifecycle-registry-WgCc3vx1.js";
import { i as mergeIdentityMarkdownContent, o as sanitizeAgentIdentityLine, t as createAgentIdentityConfig } from "./identity-file-Dnl4bkpm.js";
import { r as parseBindingSpecs, t as applyAgentBindings } from "./agents.bindings-xbGQEjU6.js";
import { r as findAgentEntryIndex, t as applyAgentConfig } from "./agents.config-CFXmxe4B.js";
import { t as migrateLegacyMainSessionKeys } from "./legacy-main-session-migration-BUkRsA4g.js";
import fs from "node:fs/promises";
//#region src/agents/agent-create.ts
const BOOTSTRAP_AGENT_ID = "main";
var DuplicateAgentError = class extends Error {};
var InvalidAgentBindingsError = class extends Error {};
function createError(reason, message, agentId) {
return {
status: "error",
reason,
message,
...agentId ? { agentId } : {}
};
}
function validateAgentIdInput(rawId, options = {}) {
const displayName = options.displayName ?? rawId;
const normalized = normalizeAgentIdStrict(rawId);
if (!normalized.ok) return {
ok: false,
reason: "invalid-name",
message: `Agent name "${displayName}" has no valid id characters. Use at least one letter a-z or digit.`
};
const agentId = normalized.value;
if (isReservedSystemAgentId(agentId)) return {
ok: false,
reason: "reserved-id",
message: `"${agentId}" is reserved`,
agentId
};
return {
ok: true,
agentId
};
}
function isInjectedBootstrapMainEntry(entry) {
return entry?.id === BOOTSTRAP_AGENT_ID && Object.keys(entry).every((key) => key === "id");
}
function describeLegacySessionOutcome(outcome) {
const claims = (outcome.sourceKeys ?? []).map((key, index) => `${outcome.paths?.[index] ?? outcome.paths?.[0] ?? "session store"}#${key}`);
switch (outcome.kind) {
case "divergent-aliases":
case "divergent-canonical": return `${outcome.kind} for ${outcome.canonicalKey ?? "the canonical session"}; preserved claims ${claims.join(", ") || "could not be reconciled"} must be quarantined`;
case "legacy-json-store": return `legacy JSON session store ${outcome.paths?.join(", ") ?? "requires import"}`;
case "store-unreadable": return `unreadable session store ${outcome.paths?.join(", ") ?? "unknown"}${outcome.detail ? ` (${outcome.detail})` : ""}`;
case "migrated-in-place":
case "migrated-cross-store":
case "canonical-exists-identical": return `legacy claim ${claims.join(", ") || outcome.canonicalKey || "requires migration"}`;
case "not-armed": return outcome.detail === "owner-unresolved" ? "legacy main sessions have no unambiguous configured owner; set agents.defaults.sessionStore.agentId to the intended live owner" : `legacy main session migration is not armed (${outcome.detail ?? "unknown reason"})`;
case "no-legacy-rows": return "the current session-store layout has no matching completed migration ledger";
}
return outcome.kind;
}
async function evaluateMainCreationGate(config, agentId) {
const roster = listAgentEntries(config).map((entry) => normalizeAgentId(entry.id));
if (agentId !== BOOTSTRAP_AGENT_ID || roster.includes(BOOTSTRAP_AGENT_ID) || !roster.some((id) => id !== BOOTSTRAP_AGENT_ID)) return;
const migration = await migrateLegacyMainSessionKeys({
cfg: config,
forceScan: true,
legacyAgentId: BOOTSTRAP_AGENT_ID,
mode: "detect"
});
const provenClean = migration.outcomes.every((outcome) => outcome.kind === "no-legacy-rows");
if (migration.armed ? !migration.ledgerComplete : !provenClean) return createError("legacy-session-migration-required", `Cannot create agent "main": ${migration.outcomes.map(describeLegacySessionOutcome).join("; ")}. Run openclaw doctor --fix, then retry.`, agentId);
if (resolveSharedAuthStoreOwnership().location !== "state-db") return createError("shared-auth-store-owned-by-main", "Cannot create agent \"main\" while agents/main/agent owns the shared auth store. Run openclaw doctor --fix to relocate shared auth, then retry.", agentId);
}
/** Read-only early check for guided flows that stage side effects before their final create. */
async function checkAgentCreationGate(agentId) {
return await withConfigMutationExclusive(async (lockedConfig) => await evaluateMainCreationGate(lockedConfig, normalizeAgentId(agentId)));
}
async function writeIdentityFile(params) {
const workspaceRoot = await root(params.workspaceDir);
let existing;
try {
existing = (await workspaceRoot.read(DEFAULT_IDENTITY_FILENAME, {
hardlinks: "reject",
nonBlockingRead: true
})).buffer.toString("utf-8");
} catch (error) {
if (!(error instanceof FsSafeError && error.code === "not-found")) throw error;
}
const content = mergeIdentityMarkdownContent(existing, params.identity);
params.beforePersistentApply?.();
await workspaceRoot.write(DEFAULT_IDENTITY_FILENAME, content, { encoding: "utf8" });
}
async function createAgent(params) {
if (params.stagedConfig && !Object.hasOwn(params, "expectedConfigHash")) throw new Error("staged agent creation requires an expected config hash");
const rawName = (params.entry?.name?.trim() || params.entry?.id || params.name || "").trim();
if (!rawName) return createError("invalid-name", "agent name is required");
const validation = validateAgentIdInput(params.entry?.id ?? rawName, { displayName: rawName });
if (!validation.ok) return createError(validation.reason, validation.message, validation.agentId);
const agentId = validation.agentId;
const isBootstrapMain = agentId === BOOTSTRAP_AGENT_ID && params.bootstrapMain === true;
const safeName = sanitizeAgentIdentityLine(rawName);
const model = normalizeOptionalString(params.model);
const identity = params.entry?.identity ?? createAgentIdentityConfig({
name: safeName,
emoji: params.emoji,
avatar: params.avatar
}) ?? { name: safeName };
const requestedWorkspace = params.entry?.workspace ?? params.workspace;
const explicitWorkspace = requestedWorkspace?.trim() ? resolveUserPath(requestedWorkspace.trim()) : void 0;
const requestedAgentDir = params.entry?.agentDir ?? params.agentDir;
const explicitAgentDir = requestedAgentDir?.trim() ? resolveUserPath(requestedAgentDir.trim()) : void 0;
const transformConfig = params.transformConfig ?? transformConfigFileWithRetry;
let configCommitRollback;
try {
return await withConfigMutationExclusive(async (lockedConfig) => {
const gateError = await evaluateMainCreationGate(lockedConfig, agentId);
if (gateError) return gateError;
params.beforePersistentApply?.();
const deletion = readAgentDeletionJournal(agentId);
if (deletion && !deletion.cleanupCompleted) return createError("deletion-pending", `agent "${agentId}" deletion cleanup is still pending`, agentId);
let tombstoneClaimed = false;
if (deletion?.cleanupCompleted && findAgentEntryIndex(listAgentEntries(lockedConfig), agentId) >= 0) {
if (!claimCompletedAgentDeletion(agentId, deletion.operationId)) throw new Error(`agent "${agentId}" deletion tombstone changed during creation`);
tombstoneClaimed = true;
}
const committed = await transformConfig({
afterWrite: { mode: "auto" },
maxAttempts: 1,
writeOptions: {
...params.bootstrapFirstAgent ? { allowedAgentRosterRemovals: [BOOTSTRAP_AGENT_ID] } : {},
...params.beforePersistentApply ? { assertConfigPathForWrite: params.beforePersistentApply } : {}
},
transform: async (currentConfig, context) => {
if (Object.hasOwn(params, "expectedConfigHash") && context.previousHash !== params.expectedConfigHash) throw new ConfigMutationConflictError("config changed before first-agent creation", { retryable: false });
const hasAuthoredRoster = params.bootstrapFirstAgent === true && hasResolvedRosterBeforeMigrations(context.snapshot);
if (params.bootstrapFirstAgent && hasAuthoredRoster) throw new DuplicateAgentError();
const bootstrappingFirstAgent = params.bootstrapFirstAgent === true;
const currentEntries = bootstrappingFirstAgent ? [] : listAgentEntries(currentConfig);
const existingIndex = findAgentEntryIndex(currentEntries, agentId);
const existingEntry = currentEntries[existingIndex];
if (isBootstrapMain && currentEntries.length > 0 && !currentEntries.some((entry) => normalizeAgentId(entry.id) === BOOTSTRAP_AGENT_ID)) throw new DuplicateAgentError();
if (existingIndex >= 0 && !isBootstrapMain) throw new DuplicateAgentError();
if (existingIndex >= 0 && isBootstrapMain && (currentEntries.length !== 1 || !isInjectedBootstrapMainEntry(existingEntry) || context.snapshot.exists)) return {
nextConfig: currentConfig,
result: {
status: "existing",
agentId,
name: existingEntry?.name ?? safeName,
workspace: resolveAgentWorkspaceDir(currentConfig, agentId),
agentDir: resolveAgentDir(currentConfig, agentId),
bootstrapPending: false
}
};
const workspaceDir = explicitWorkspace ?? resolveAgentWorkspaceDir(currentConfig, agentId);
const agentDir = explicitAgentDir ?? resolveAgentDir(currentConfig, agentId);
const materializeInjectedMain = existingIndex >= 0 && isBootstrapMain && isInjectedBootstrapMainEntry(existingEntry) && !context.snapshot.exists;
const creationBase = bootstrappingFirstAgent ? {
...currentConfig,
agents: {
...currentConfig.agents,
entries: {},
list: void 0
}
} : params.stagedConfig ?? currentConfig;
let nextConfig = existingIndex < 0 || materializeInjectedMain ? applyAgentConfig(creationBase, {
agentId,
name: safeName,
workspace: workspaceDir,
agentDir,
model,
identity
}) : creationBase;
if (params.entry) {
const { default: _retiredDefault, ...stagedEntry } = params.entry;
const list = listAgentEntries(nextConfig);
const index = findAgentEntryIndex(list, agentId);
list[index] = {
...list[index],
...stagedEntry,
id: agentId,
name: safeName,
workspace: workspaceDir,
agentDir,
identity
};
const { list: _legacyList, ...agentsConfig } = nextConfig.agents ?? {};
nextConfig = {
...nextConfig,
agents: {
...agentsConfig,
entries: toAgentEntriesRecord(list)
}
};
}
const bindingParse = parseBindingSpecs({
agentId,
specs: params.bindingSpecs,
config: nextConfig
});
if (bindingParse.errors.length > 0) throw new InvalidAgentBindingsError(bindingParse.errors.join("\n"));
const bindingResult = bindingParse.bindings.length ? applyAgentBindings(nextConfig, bindingParse.bindings) : void 0;
nextConfig = bindingResult?.config ?? nextConfig;
const skipBootstrap = params.skipBootstrap ?? nextConfig.agents?.defaults?.skipBootstrap;
params.beforePersistentApply?.();
const workspace = await ensureAgentWorkspace({
dir: workspaceDir,
beforePersistentApply: params.beforePersistentApply,
ensureBootstrapFiles: !skipBootstrap,
skipOptionalBootstrapFiles: params.skipOptionalBootstrapFiles ?? nextConfig.agents?.defaults?.skipOptionalBootstrapFiles
});
if (workspace.dir !== workspaceDir) {
const entries = listAgentEntries(nextConfig);
const entryIndex = findAgentEntryIndex(entries, agentId);
const currentEntry = entries[entryIndex];
if (entryIndex >= 0 && currentEntry) {
entries[entryIndex] = {
...currentEntry,
id: agentId,
workspace: workspace.dir
};
const { list: _legacyList, ...agentsConfig } = nextConfig.agents ?? {};
nextConfig = {
...nextConfig,
agents: {
...agentsConfig,
entries: toAgentEntriesRecord(entries)
}
};
}
}
params.beforePersistentApply?.();
await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true });
if (!workspace.bootstrapPending && !skipBootstrap) await writeIdentityFile({
workspaceDir: workspace.dir,
identity,
beforePersistentApply: params.beforePersistentApply
});
params.beforePersistentApply?.();
const preparedRollback = await params.prepareConfigCommit?.();
configCommitRollback = typeof preparedRollback === "function" ? preparedRollback : void 0;
return {
nextConfig,
result: {
status: existingIndex >= 0 ? "existing" : "created",
agentId,
name: safeName,
workspace: workspace.dir,
agentDir,
...model ? { model } : {},
bootstrapPending: workspace.bootstrapPending === true,
...bindingResult ? { bindingResult } : {}
}
};
}
});
configCommitRollback = void 0;
if (deletion?.cleanupCompleted && !tombstoneClaimed && committed.result?.status === "created" && !claimCompletedAgentDeletion(agentId, deletion.operationId)) throw new Error(`agent "${agentId}" deletion tombstone changed during creation`);
const result = committed.result;
if (result.status === "created") recordAgentProvenance(agentId, params.provenance ?? { createdVia: "operator" });
return {
...result,
config: committed.nextConfig,
...typeof committed.persistedHash === "string" ? { configHash: committed.persistedHash } : {}
};
});
} catch (error) {
if (configCommitRollback) try {
await configCommitRollback();
} catch (rollbackError) {
throw new Error(`${String(error)}\nstaged config rollback failed: ${String(rollbackError)}`, { cause: rollbackError });
}
if (error instanceof DuplicateAgentError) return createError("already-exists", `agent "${agentId}" already exists`, agentId);
if (error instanceof InvalidAgentBindingsError) return createError("invalid-bindings", error.message, agentId);
if (error instanceof FsSafeError) return createError("unsafe-identity-file", `unsafe workspace file "${DEFAULT_IDENTITY_FILENAME}"`, agentId);
throw error;
}
}
//#endregion
export { createAgent as n, validateAgentIdInput as r, checkAgentCreationGate as t };