mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 04:31:42 +00:00
* refactor(agents): require explicit roster defaults * feat(onboard): create named first roster agent * refactor(agents): remove runtime main fallbacks * style(agents): apply roster refactor formatting * refactor(agents): finish roster-only runtime sweep * fix(doctor): migrate legacy main session sqlite * fix(doctor): harden roster session migrations * fix(onboard): commit first agent atomically * fix(config): support empty-roster analysis * fix(agents): preserve legacy main state during creation * fix(setup): materialize baseline agent roster * fix(agents): harden legacy default transfer recovery * fix(agents): simplify roster-only legacy compatibility * fix(agents): preserve staged first-agent entries * fix(config): migrate persisted implicit-main rosters * fix(config): preserve staged empty rosters * fix(agents): finalize roster-only upgrade paths * fix(sessions): close legacy main migration outcomes * fix(config): migrate legacy roster markers at load * fix(sessions): preserve roster upgrade history * refactor(sessions): restore lean legacy main compatibility * fix(setup): prepare first-agent credentials before publish * fix(config): stabilize roster snapshot migration * refactor(sessions): shrink legacy main compatibility * fix(agents): restore roster compatibility fidelity * fix(sessions): preserve divergent legacy history * refactor(agents): narrow roster-only scope * fix(config): isolate roster migration * test(agents): align roster-only fixtures * fix(agents): keep main agent undeletable * fix(agents): harden roster migration invariants * fix(agents): close setup and audit scope gaps * fix(cron): scope session reaper throttles by agent * fix(agents): preserve scoped owner precedence * fix(config): preserve authored config ownership * fix(setup): keep default workspace and roster in sync * fix(setup): preserve default entry workspace on bare runs * fix(agents): adapt roster rebase to keyed entries * fix(agents): honor both roster representations * fix(agents): route roster reads through shared helpers * fix(config): preserve canonical roster writes * fix(cron): resolve dynamic default for session reaper * fix(agents): close dynamic default migration gaps * fix(agents): align scoped session ownership * fix(sessions): preserve legacy main directory casing * fix(agents): align cron and legacy auth ownership * fix(setup): provision the committed default workspace * fix(cron): align scoped ownership and reaping * fix(cron): treat blank agent ids as absent * fix(cron): retain configured session-store owners * fix(agents): repair roster-aware CI boundaries * fix(cron): preserve scoped ownership resolution * fix(agents): preserve rosterless maintenance paths * fix(agents): propagate roster ownership through runtime boundaries * fix(agents): preserve roster ownership across runtime paths * fix(agents): harden roster diagnostics and legacy routing * fix(agents): remove redundant diagnostic import * test(agents): type CLI policy fixture explicitly * fix(config): preserve canonical roster mutation identity * fix(doctor): read canonical agent rosters consistently * fix(config): resolve compound roster unsets safely * fix(config): finalize main-session reconciliation * fix(doctor): read canonical session state safely * fix(sessions): preserve current visibility alias * fix(config): track roster include provenance * test(config): type roster provenance cases * fix(config): refine roster include ownership * fix(agents): preserve staged roster invariants * test(config): align fixtures with explicit roster ownership * test(node-host): preserve optional plan typing * fix(config): preserve authored roster projections * test(config): keep raw roster fixtures explicit * test(config): normalize rosters at runtime fixtures * fix(config): protect authored roster ownership * fix(agents): require explicit session ownership * fix(agents): enforce scoped roster ownership * fix(sessions): merge fixed-store agent partitions * fix(agents): harden roster ownership boundaries * fix(config): reject ambiguous roster projections * fix(sessions): preserve persisted store ownership * fix(sessions): keep collision diagnostics additive * fix(security): scan malformed roster workspaces * test(config): align snapshot fixtures after rebase * test(agents): use explicit roster fixtures * fix(config): harden roster diagnostic boundaries * fix(sessions): isolate fixed-store agent databases * test(agents): type malformed default markers * refactor(sessions): extract store collision resolution * test(system-agent): split oversized setup coverage * style(system-agent): format split setup suite * fix(sessions): preserve promoted store ownership * fix(sessions): derive scoped owner before target * fix(sessions): preserve explicit sqlite ownership * fix(agents): restore roster compatibility across CI * fix(agents): enforce roster-owned runtime boundaries * fix(agents): satisfy default lookup lint * test(sessions): split known-owner coverage * fix(state): satisfy path identity lint * fix(agents): preserve malformed roster safety boundaries * fix(agents): restore roster compatibility at runtime boundaries * fix(config): satisfy roster boundary type checks * fix(agents): preserve roster ownership across runtime probes Setup inference probes now execute as the configured roster owner. Malformed agent-prefixed session rows are intentionally omitted by the fail-closed visibility contract rather than normalized by tests. * fix(agents): satisfy session list owner lint * fix(agents): preserve roster-owned runtime boundaries Restore shared logical rows for exact SQLite session locators while keeping their physical database owner separate. The ownership regression test now constructs an explicit sole-owner database directly instead of relying on first-touch capture, matching the intentional shared-store contract. * fix(sessions): preserve multiply owned exact stores * fix(sessions): restore runtime owner boundaries Keep incognito sentinels agent-owned, fold default-agent approvals into the global snapshot, and preserve the configless legacy-main CLI policy fallback. Also repair the existing CLI watchdog test lifecycle so the compact shard observes its timeout without an unawaited assertion or async timer stall; product behavior is unchanged by that test-only fix. * test(ci): align owner-scoped fixtures These assertions are unchanged. The fixtures now declare the intended non-default runner, expose the session-key constant imported by production status code, and select the main approvals bucket explicitly on Windows. * fix(agents): close final roster ownership gaps
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
/**
|
|
* Minimal setup command.
|
|
*
|
|
* Ensures config, default workspace, and session directories exist without
|
|
* running the full onboarding wizard.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import {
|
|
listAgentEntries,
|
|
resolveAgentEntry,
|
|
resolveDefaultAgentId,
|
|
toAgentEntriesRecord,
|
|
} from "../agents/agent-scope-config.js";
|
|
import { formatCliCommand } from "../cli/command-format.js";
|
|
import {
|
|
configIncludeOwnsAgentRoster,
|
|
hasResolvedRosterBeforeMigrations,
|
|
} from "../config/agent-roster-provenance.js";
|
|
import type { ConfigWriteOptions, ReadConfigFileSnapshotForWriteResult } from "../config/io.js";
|
|
import { migratePersistedImplicitMainRoster } from "../config/legacy.js";
|
|
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
|
|
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { defaultRuntime } from "../runtime.js";
|
|
import { createLazyImportLoader } from "../shared/lazy-promise.js";
|
|
import { shortenHomePath } from "../utils.js";
|
|
|
|
type ConfigIO = {
|
|
configPath: string;
|
|
readConfigFileSnapshotForWrite: () => Promise<ReadConfigFileSnapshotForWriteResult>;
|
|
};
|
|
|
|
type ReplaceConfigFile = (params: {
|
|
nextConfig: OpenClawConfig;
|
|
snapshot: ConfigFileSnapshot;
|
|
afterWrite: { mode: "auto" };
|
|
writeOptions: ConfigWriteOptions;
|
|
}) => Promise<unknown>;
|
|
|
|
type EnsureAgentWorkspace = (params: {
|
|
dir: string;
|
|
ensureBootstrapFiles?: boolean;
|
|
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
|
|
}) => Promise<{ dir: string }>;
|
|
|
|
type SetupCommandDeps = {
|
|
createConfigIO?: () => ConfigIO;
|
|
defaultAgentWorkspaceDir?: string | (() => string | Promise<string>);
|
|
ensureAgentWorkspace?: EnsureAgentWorkspace;
|
|
formatConfigPath?: (path: string) => string;
|
|
logConfigUpdated?: (
|
|
runtime: RuntimeEnv,
|
|
opts: { path?: string; suffix?: string },
|
|
) => void | Promise<void>;
|
|
mkdir?: (dir: string, options: { recursive: true }) => Promise<unknown>;
|
|
resolveSessionTranscriptsDir?: (agentId: string) => string | Promise<string>;
|
|
replaceConfigFile?: ReplaceConfigFile;
|
|
};
|
|
|
|
type AgentWorkspaceModule = typeof import("../agents/workspace.js");
|
|
type ConfigIOModule = typeof import("../config/config.js");
|
|
type ConfigLoggingModule = typeof import("../config/logging.js");
|
|
|
|
const agentWorkspaceModuleLoader = createLazyImportLoader<AgentWorkspaceModule>(
|
|
() => import("../agents/workspace.js"),
|
|
);
|
|
const configIOModuleLoader = createLazyImportLoader<ConfigIOModule>(
|
|
() => import("../config/config.js"),
|
|
);
|
|
const configLoggingModuleLoader = createLazyImportLoader<ConfigLoggingModule>(
|
|
() => import("../config/logging.js"),
|
|
);
|
|
|
|
// Keep setup's cold path small; config/workspace modules are loaded only when
|
|
// their default dependency is actually needed.
|
|
function loadAgentWorkspaceModule(): Promise<AgentWorkspaceModule> {
|
|
return agentWorkspaceModuleLoader.load();
|
|
}
|
|
|
|
function loadConfigIOModule(): Promise<ConfigIOModule> {
|
|
return configIOModuleLoader.load();
|
|
}
|
|
|
|
function loadConfigLoggingModule(): Promise<ConfigLoggingModule> {
|
|
return configLoggingModuleLoader.load();
|
|
}
|
|
|
|
async function createDefaultConfigIO(): Promise<ConfigIO> {
|
|
const { createConfigIO } = await loadConfigIOModule();
|
|
return createConfigIO();
|
|
}
|
|
|
|
async function resolveDefaultAgentWorkspaceDir(deps: SetupCommandDeps): Promise<string> {
|
|
const override = deps.defaultAgentWorkspaceDir;
|
|
if (typeof override === "string") {
|
|
return override;
|
|
}
|
|
if (typeof override === "function") {
|
|
return await override();
|
|
}
|
|
const { DEFAULT_AGENT_WORKSPACE_DIR } = await loadAgentWorkspaceModule();
|
|
return DEFAULT_AGENT_WORKSPACE_DIR;
|
|
}
|
|
|
|
async function ensureDefaultAgentWorkspace(
|
|
params: Parameters<EnsureAgentWorkspace>[0],
|
|
): ReturnType<EnsureAgentWorkspace> {
|
|
const { ensureAgentWorkspace } = await loadAgentWorkspaceModule();
|
|
return ensureAgentWorkspace(params);
|
|
}
|
|
|
|
async function writeDefaultConfigFile(params: Parameters<ReplaceConfigFile>[0]): Promise<void> {
|
|
const { replaceConfigFile } = await loadConfigIOModule();
|
|
await replaceConfigFile(params);
|
|
}
|
|
|
|
async function formatDefaultConfigPath(configPath: string): Promise<string> {
|
|
const { formatConfigPath } = await loadConfigLoggingModule();
|
|
return formatConfigPath(configPath);
|
|
}
|
|
|
|
async function logDefaultConfigUpdated(
|
|
runtime: RuntimeEnv,
|
|
opts: { path?: string; suffix?: string },
|
|
): Promise<void> {
|
|
const { logConfigUpdated } = await loadConfigLoggingModule();
|
|
logConfigUpdated(runtime, opts);
|
|
}
|
|
|
|
async function resolveDefaultSessionTranscriptsDir(agentId: string): Promise<string> {
|
|
const { resolveSessionTranscriptsDirForAgent } = await import("../config/sessions.js");
|
|
return resolveSessionTranscriptsDirForAgent(agentId);
|
|
}
|
|
|
|
/** Prepares config, workspace, and session directories for a usable installation. */
|
|
export async function setupCommand(
|
|
opts?: { workspace?: string },
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps: SetupCommandDeps = {},
|
|
) {
|
|
const desiredWorkspace =
|
|
typeof opts?.workspace === "string" && opts.workspace.trim()
|
|
? opts.workspace.trim()
|
|
: undefined;
|
|
|
|
const io = deps.createConfigIO?.() ?? (await createDefaultConfigIO());
|
|
const configPath = io.configPath;
|
|
const prepared = await io.readConfigFileSnapshotForWrite();
|
|
const snapshot = prepared.snapshot;
|
|
if (snapshot.exists && !snapshot.valid) {
|
|
const formatConfigPath = deps.formatConfigPath ?? formatDefaultConfigPath;
|
|
runtime.error(
|
|
`Config invalid at ${await formatConfigPath(configPath)}. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run setup.`,
|
|
);
|
|
runtime.exit(1);
|
|
return;
|
|
}
|
|
|
|
const resolvedConfig = snapshot.config;
|
|
const shouldPersistRoster =
|
|
!snapshot.exists ||
|
|
(!hasResolvedRosterBeforeMigrations(snapshot) && !configIncludeOwnsAgentRoster(snapshot));
|
|
const cfg = shouldPersistRoster
|
|
? (migratePersistedImplicitMainRoster(snapshot.sourceConfig).config as OpenClawConfig)
|
|
: snapshot.sourceConfig;
|
|
const authoredDefaults = cfg.agents?.defaults ?? {};
|
|
const resolvedDefaults = resolvedConfig.agents?.defaults ?? authoredDefaults;
|
|
const defaultEntry = resolveAgentEntry(resolvedConfig, resolveDefaultAgentId(resolvedConfig));
|
|
const defaultEntryWorkspace = defaultEntry?.workspace?.trim();
|
|
const configuredWorkspace = defaultEntryWorkspace || resolvedDefaults.workspace;
|
|
|
|
const workspace =
|
|
desiredWorkspace ?? configuredWorkspace ?? (await resolveDefaultAgentWorkspaceDir(deps));
|
|
// Bare setup is observational for an established roster. Only a caller
|
|
// override or fresh bootstrap owns a persisted workspace change.
|
|
const shouldWriteWorkspace =
|
|
!snapshot.exists || (desiredWorkspace !== undefined && configuredWorkspace !== workspace);
|
|
const shouldWriteGatewayMode = resolvedConfig.gateway?.mode === undefined;
|
|
const writeInheritedWorkspaceOverride =
|
|
snapshot.exists &&
|
|
shouldWriteWorkspace &&
|
|
!defaultEntryWorkspace &&
|
|
configIncludeOwnsAgentRoster(snapshot);
|
|
|
|
// Keep the candidate runtime-shaped. replaceConfigFile persists only its
|
|
// diff against snapshot.parsed, never resolved include/env values wholesale.
|
|
let next: OpenClawConfig = snapshot.exists ? resolvedConfig : cfg;
|
|
if (shouldPersistRoster) {
|
|
const { list: _legacyList, ...agents } = next.agents ?? {};
|
|
next = {
|
|
...next,
|
|
agents: { ...agents, entries: toAgentEntriesRecord(listAgentEntries(cfg)) },
|
|
};
|
|
}
|
|
if (shouldWriteWorkspace) {
|
|
if (!writeInheritedWorkspaceOverride) {
|
|
const roster = structuredClone(listAgentEntries(next));
|
|
if (!snapshot.exists || Boolean(defaultEntryWorkspace)) {
|
|
for (const entry of roster) {
|
|
if (entry.default === true) {
|
|
// Fresh bootstrap and explicitly entry-owned workspaces stay aligned.
|
|
// Inherited defaults must not turn an include-owned roster into a roster write.
|
|
entry.workspace = workspace;
|
|
}
|
|
}
|
|
}
|
|
const entries = roster.length > 0 ? toAgentEntriesRecord(roster) : undefined;
|
|
const { list: _legacyList, ...agents } = next.agents ?? {};
|
|
next = {
|
|
...next,
|
|
agents: {
|
|
...agents,
|
|
defaults: { ...agents.defaults, workspace },
|
|
...(entries ? { entries } : {}),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
if (shouldWriteGatewayMode) {
|
|
next = { ...next, gateway: { ...next.gateway, mode: "local" } };
|
|
}
|
|
|
|
if (!snapshot.exists) {
|
|
const { ensureOnboardingAgent } = await import("./onboard-agent.js");
|
|
next = (await ensureOnboardingAgent({ config: next, workspace, baseConfig: cfg })).config;
|
|
}
|
|
|
|
if (!snapshot.exists || shouldPersistRoster || shouldWriteWorkspace || shouldWriteGatewayMode) {
|
|
// Preserve all existing config fields and touch only workspace/gateway mode
|
|
// defaults that this command owns.
|
|
const replaceConfig = deps.replaceConfigFile ?? writeDefaultConfigFile;
|
|
await replaceConfig({
|
|
nextConfig: next,
|
|
snapshot,
|
|
afterWrite: { mode: "auto" },
|
|
writeOptions: {
|
|
...prepared.writeOptions,
|
|
...(snapshot.exists && shouldPersistRoster
|
|
? {
|
|
explicitSetPaths: [["agents", "entries"]],
|
|
explicitSetValueSource: cfg,
|
|
}
|
|
: {}),
|
|
...(writeInheritedWorkspaceOverride
|
|
? {
|
|
allowIncludeAncestorExplicitSetPaths: true,
|
|
explicitSetPaths: [["agents", "defaults", "workspace"]],
|
|
explicitSetValueSource: { agents: { defaults: { workspace } } },
|
|
}
|
|
: {}),
|
|
},
|
|
});
|
|
if (!snapshot.exists) {
|
|
const formatConfigPath = deps.formatConfigPath ?? formatDefaultConfigPath;
|
|
runtime.log(`Wrote ${await formatConfigPath(configPath)}`);
|
|
} else {
|
|
const updates: string[] = [];
|
|
if (shouldWriteWorkspace) {
|
|
updates.push("set agents.defaults.workspace");
|
|
}
|
|
if (shouldWriteGatewayMode) {
|
|
updates.push("set gateway.mode");
|
|
}
|
|
const suffix = updates.length > 0 ? `(${updates.join(", ")})` : undefined;
|
|
await (deps.logConfigUpdated ?? logDefaultConfigUpdated)(runtime, {
|
|
path: configPath,
|
|
suffix,
|
|
});
|
|
}
|
|
} else {
|
|
const formatConfigPath = deps.formatConfigPath ?? formatDefaultConfigPath;
|
|
runtime.log(`Config OK: ${await formatConfigPath(configPath)}`);
|
|
}
|
|
|
|
const ws = await (deps.ensureAgentWorkspace ?? ensureDefaultAgentWorkspace)({
|
|
dir: workspace,
|
|
ensureBootstrapFiles: !resolvedDefaults.skipBootstrap,
|
|
skipOptionalBootstrapFiles: resolvedDefaults.skipOptionalBootstrapFiles,
|
|
});
|
|
runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);
|
|
|
|
const defaultAgentId = resolveDefaultAgentId(next);
|
|
const sessionsDir = await (
|
|
deps.resolveSessionTranscriptsDir ?? resolveDefaultSessionTranscriptsDir
|
|
)(defaultAgentId);
|
|
await (deps.mkdir ?? fs.mkdir)(sessionsDir, { recursive: true });
|
|
runtime.log(`Sessions OK: ${shortenHomePath(sessionsDir)}`);
|
|
runtime.log("");
|
|
runtime.log("Setup complete: config, workspace, and session directories are ready.");
|
|
runtime.log(`Next guided path: ${formatCliCommand("openclaw onboard")}.`);
|
|
runtime.log(
|
|
`Next targeted changes: ${formatCliCommand("openclaw configure")} for models, channels, Gateway, plugins, skills, and health checks.`,
|
|
);
|
|
runtime.log(`Add a chat channel later: ${formatCliCommand("openclaw channels add")}.`);
|
|
}
|