mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 15:43:57 -06:00
82d1a03f25
* 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
252 lines
8.1 KiB
TypeScript
252 lines
8.1 KiB
TypeScript
// Agent config mutation and summary builders used by `openclaw agents` commands.
|
|
import {
|
|
normalizeOptionalString,
|
|
resolvePrimaryStringValue,
|
|
} from "@openclaw/normalization-core/string-coerce";
|
|
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
|
import {
|
|
listAgentEntries,
|
|
resolveAgentDir,
|
|
resolveAgentWorkspaceDir,
|
|
resolveDefaultAgentId,
|
|
toAgentEntriesRecord,
|
|
} from "../agents/agent-scope.js";
|
|
import { resolveAgentAvatarUrlFromSource } from "../agents/identity-avatar-file.js";
|
|
import type { AgentIdentityFile } from "../agents/identity-file.js";
|
|
import { identityHasValues, loadAgentIdentityFromWorkspace } from "../agents/identity-file.js";
|
|
import { listRouteBindings } from "../config/bindings.js";
|
|
import type { IdentityConfig } from "../config/types.base.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizeAgentId } from "../routing/session-key.js";
|
|
|
|
export type AgentSummary = {
|
|
id: string;
|
|
name?: string;
|
|
identityName?: string;
|
|
identityEmoji?: string;
|
|
identityAvatarUrl?: string;
|
|
identitySource?: "identity" | "config";
|
|
workspace: string;
|
|
agentDir: string;
|
|
model?: string;
|
|
bindings: number;
|
|
bindingDetails?: string[];
|
|
routes?: string[];
|
|
providers?: string[];
|
|
isDefault: boolean;
|
|
};
|
|
|
|
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
|
|
|
export type AgentIdentity = AgentIdentityFile;
|
|
export { listAgentEntries };
|
|
|
|
/** Find a configured agent entry by normalized id. */
|
|
export function findAgentEntryIndex(list: AgentEntry[], agentId: string): number {
|
|
const id = normalizeAgentId(agentId);
|
|
return list.findIndex((entry) => normalizeAgentId(entry.id) === id);
|
|
}
|
|
|
|
function resolveAgentModel(cfg: OpenClawConfig, agentId: string) {
|
|
const entry = listAgentEntries(cfg).find(
|
|
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
|
);
|
|
const entryPrimary = resolvePrimaryStringValue(entry?.model);
|
|
if (entryPrimary) {
|
|
return entryPrimary;
|
|
}
|
|
return resolvePrimaryStringValue(cfg.agents?.defaults?.model);
|
|
}
|
|
|
|
/** Load non-empty identity metadata from a workspace identity file. */
|
|
export function loadAgentIdentity(workspace: string): AgentIdentity | null {
|
|
const parsed = loadAgentIdentityFromWorkspace(workspace);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
return identityHasValues(parsed) ? parsed : null;
|
|
}
|
|
|
|
/** Build config-derived summaries for text/JSON agent listing. */
|
|
export function buildAgentSummaries(cfg: OpenClawConfig): AgentSummary[] {
|
|
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
|
const configuredAgents = listAgentEntries(cfg);
|
|
const orderedIds =
|
|
configuredAgents.length > 0
|
|
? configuredAgents.map((agent) => normalizeAgentId(agent.id))
|
|
: [defaultAgentId];
|
|
const bindingCounts = new Map<string, number>();
|
|
for (const binding of listRouteBindings(cfg)) {
|
|
const agentId = normalizeAgentId(binding.agentId);
|
|
bindingCounts.set(agentId, (bindingCounts.get(agentId) ?? 0) + 1);
|
|
}
|
|
|
|
const ordered = uniqueStrings(orderedIds);
|
|
|
|
return ordered.map((id) => {
|
|
const workspace = resolveAgentWorkspaceDir(cfg, id);
|
|
const identity = loadAgentIdentity(workspace);
|
|
const configIdentity = configuredAgents.find(
|
|
(agent) => normalizeAgentId(agent.id) === id,
|
|
)?.identity;
|
|
const identityName = identity?.name ?? configIdentity?.name?.trim();
|
|
const identityEmoji = identity?.emoji ?? configIdentity?.emoji?.trim();
|
|
const identityAvatarUrl = resolveAgentAvatarUrlFromSource(
|
|
cfg,
|
|
id,
|
|
identity?.avatar ?? configIdentity?.avatar,
|
|
);
|
|
const identitySource = identity
|
|
? "identity"
|
|
: configIdentity && (identityName || identityEmoji || identityAvatarUrl)
|
|
? "config"
|
|
: undefined;
|
|
const summary: AgentSummary = {
|
|
id,
|
|
name: normalizeOptionalString(
|
|
configuredAgents.find((agent) => normalizeAgentId(agent.id) === id)?.name,
|
|
),
|
|
identityName,
|
|
identityEmoji,
|
|
identitySource,
|
|
workspace,
|
|
agentDir: resolveAgentDir(cfg, id),
|
|
model: resolveAgentModel(cfg, id),
|
|
bindings: bindingCounts.get(id) ?? 0,
|
|
isDefault: id === defaultAgentId,
|
|
};
|
|
if (identityAvatarUrl) {
|
|
summary.identityAvatarUrl = identityAvatarUrl;
|
|
}
|
|
return summary;
|
|
});
|
|
}
|
|
|
|
/** Add or update one agent entry. The first roster entry becomes the explicit default. */
|
|
export function applyAgentConfig(
|
|
cfg: OpenClawConfig,
|
|
params: {
|
|
agentId: string;
|
|
name?: string;
|
|
workspace?: string;
|
|
agentDir?: string;
|
|
model?: string | null;
|
|
identity?: IdentityConfig;
|
|
},
|
|
): OpenClawConfig {
|
|
const agentId = normalizeAgentId(params.agentId);
|
|
const name = params.name?.trim();
|
|
const list = listAgentEntries(cfg);
|
|
const index = findAgentEntryIndex(list, agentId);
|
|
const base = (index >= 0 ? list[index] : undefined) ?? {
|
|
id: agentId,
|
|
...(list.length === 0 ? { default: true } : {}),
|
|
};
|
|
const mergedIdentity = params.identity ? { ...base.identity, ...params.identity } : undefined;
|
|
const nextEntry: AgentEntry = {
|
|
...base,
|
|
...(name ? { name } : {}),
|
|
...(params.workspace ? { workspace: params.workspace } : {}),
|
|
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
|
...(mergedIdentity ? { identity: mergedIdentity } : {}),
|
|
};
|
|
// Model is tri-state: omission preserves the override, null restores inheritance.
|
|
if (params.model === null) {
|
|
delete nextEntry.model;
|
|
} else if (params.model !== undefined) {
|
|
nextEntry.model = params.model;
|
|
}
|
|
const nextList = [...list];
|
|
if (index >= 0) {
|
|
nextList[index] = nextEntry;
|
|
} else {
|
|
nextList.push(nextEntry);
|
|
}
|
|
const { list: _legacyList, ...agentsConfig } = cfg.agents ?? {};
|
|
return {
|
|
...cfg,
|
|
agents: {
|
|
...agentsConfig,
|
|
entries: toAgentEntriesRecord(nextList),
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Remove an agent and any config references that route or allow traffic to it. */
|
|
export function pruneAgentConfig(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): {
|
|
config: OpenClawConfig;
|
|
removedBindings: number;
|
|
removedAllow: number;
|
|
} {
|
|
const id = normalizeAgentId(agentId);
|
|
const agents = listAgentEntries(cfg);
|
|
const pruneAllowAgents = (allowAgents: string[] | undefined) =>
|
|
allowAgents?.filter((entry) => {
|
|
const trimmed = entry.trim();
|
|
return !trimmed || trimmed === "*" || normalizeAgentId(trimmed) !== id;
|
|
});
|
|
const nextAgentsList = [];
|
|
for (const entry of agents) {
|
|
if (normalizeAgentId(entry.id) === id) {
|
|
continue;
|
|
}
|
|
nextAgentsList.push(
|
|
entry.subagents?.allowAgents
|
|
? {
|
|
...entry,
|
|
subagents: {
|
|
...entry.subagents,
|
|
allowAgents: pruneAllowAgents(entry.subagents.allowAgents),
|
|
},
|
|
}
|
|
: entry,
|
|
);
|
|
}
|
|
const nextAgents = nextAgentsList.length > 0 ? toAgentEntriesRecord(nextAgentsList) : undefined;
|
|
|
|
const bindings = cfg.bindings ?? [];
|
|
const filteredBindings = bindings.filter((binding) => normalizeAgentId(binding.agentId) !== id);
|
|
|
|
const allow = cfg.tools?.agentToAgent?.allow ?? [];
|
|
const filteredAllow = allow.filter((entry) => entry !== id);
|
|
|
|
const nextDefaults = cfg.agents?.defaults?.subagents?.allowAgents
|
|
? {
|
|
...cfg.agents.defaults,
|
|
subagents: {
|
|
...cfg.agents.defaults.subagents,
|
|
allowAgents: pruneAllowAgents(cfg.agents.defaults.subagents.allowAgents),
|
|
},
|
|
}
|
|
: cfg.agents?.defaults;
|
|
const { list: _legacyList, ...agentsConfig } = cfg.agents ?? {};
|
|
const nextAgentsConfig = cfg.agents
|
|
? { ...agentsConfig, defaults: nextDefaults, entries: nextAgents }
|
|
: nextAgents
|
|
? { entries: nextAgents }
|
|
: undefined;
|
|
const nextTools = cfg.tools?.agentToAgent
|
|
? {
|
|
...cfg.tools,
|
|
agentToAgent: {
|
|
...cfg.tools.agentToAgent,
|
|
allow: filteredAllow.length > 0 ? filteredAllow : undefined,
|
|
},
|
|
}
|
|
: cfg.tools;
|
|
|
|
return {
|
|
config: {
|
|
...cfg,
|
|
agents: nextAgentsConfig,
|
|
bindings: filteredBindings.length > 0 ? filteredBindings : undefined,
|
|
tools: nextTools,
|
|
},
|
|
removedBindings: bindings.length - filteredBindings.length,
|
|
removedAllow: allow.length - filteredAllow.length,
|
|
};
|
|
}
|