Add stripNullBytes() helper and apply it to all return paths in resolveAgentWorkspaceDir() including configured, default, and state-dir-derived paths. Null bytes in paths cause ENOTDIR errors when Node tries to resolve them as directories.
246 lines
8.0 KiB
TypeScript
246 lines
8.0 KiB
TypeScript
import path from "node:path";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
|
|
import { resolveStateDir } from "../config/paths.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import {
|
|
DEFAULT_AGENT_ID,
|
|
normalizeAgentId,
|
|
parseAgentSessionKey,
|
|
} from "../routing/session-key.js";
|
|
import { resolveUserPath } from "../utils.js";
|
|
import { normalizeSkillFilter } from "./skills/filter.js";
|
|
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
|
|
const log = createSubsystemLogger("agent-scope");
|
|
|
|
/** Strip null bytes from paths to prevent ENOTDIR errors. */
|
|
function stripNullBytes(s: string): string {
|
|
// eslint-disable-next-line no-control-regex
|
|
return s.replace(/\0/g, "");
|
|
}
|
|
|
|
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
|
|
|
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
|
|
|
type ResolvedAgentConfig = {
|
|
name?: string;
|
|
workspace?: string;
|
|
agentDir?: string;
|
|
model?: AgentEntry["model"];
|
|
skills?: AgentEntry["skills"];
|
|
memorySearch?: AgentEntry["memorySearch"];
|
|
humanDelay?: AgentEntry["humanDelay"];
|
|
heartbeat?: AgentEntry["heartbeat"];
|
|
identity?: AgentEntry["identity"];
|
|
groupChat?: AgentEntry["groupChat"];
|
|
subagents?: AgentEntry["subagents"];
|
|
sandbox?: AgentEntry["sandbox"];
|
|
tools?: AgentEntry["tools"];
|
|
};
|
|
|
|
let defaultAgentWarned = false;
|
|
|
|
export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] {
|
|
const list = cfg.agents?.list;
|
|
if (!Array.isArray(list)) {
|
|
return [];
|
|
}
|
|
return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"));
|
|
}
|
|
|
|
export function listAgentIds(cfg: OpenClawConfig): string[] {
|
|
const agents = listAgentEntries(cfg);
|
|
if (agents.length === 0) {
|
|
return [DEFAULT_AGENT_ID];
|
|
}
|
|
const seen = new Set<string>();
|
|
const ids: string[] = [];
|
|
for (const entry of agents) {
|
|
const id = normalizeAgentId(entry?.id);
|
|
if (seen.has(id)) {
|
|
continue;
|
|
}
|
|
seen.add(id);
|
|
ids.push(id);
|
|
}
|
|
return ids.length > 0 ? ids : [DEFAULT_AGENT_ID];
|
|
}
|
|
|
|
export function resolveDefaultAgentId(cfg: OpenClawConfig): string {
|
|
const agents = listAgentEntries(cfg);
|
|
if (agents.length === 0) {
|
|
return DEFAULT_AGENT_ID;
|
|
}
|
|
const defaults = agents.filter((agent) => agent?.default);
|
|
if (defaults.length > 1 && !defaultAgentWarned) {
|
|
defaultAgentWarned = true;
|
|
log.warn("Multiple agents marked default=true; using the first entry as default.");
|
|
}
|
|
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
|
|
}
|
|
|
|
export function resolveSessionAgentIds(params: {
|
|
sessionKey?: string;
|
|
config?: OpenClawConfig;
|
|
agentId?: string;
|
|
}): {
|
|
defaultAgentId: string;
|
|
sessionAgentId: string;
|
|
} {
|
|
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
|
const explicitAgentIdRaw =
|
|
typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : "";
|
|
const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null;
|
|
const sessionKey = params.sessionKey?.trim();
|
|
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
|
|
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
|
|
const sessionAgentId =
|
|
explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId);
|
|
return { defaultAgentId, sessionAgentId };
|
|
}
|
|
|
|
export function resolveSessionAgentId(params: {
|
|
sessionKey?: string;
|
|
config?: OpenClawConfig;
|
|
}): string {
|
|
return resolveSessionAgentIds(params).sessionAgentId;
|
|
}
|
|
|
|
function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined {
|
|
const id = normalizeAgentId(agentId);
|
|
return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
|
}
|
|
|
|
export function resolveAgentConfig(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): ResolvedAgentConfig | undefined {
|
|
const id = normalizeAgentId(agentId);
|
|
const entry = resolveAgentEntry(cfg, id);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
name: typeof entry.name === "string" ? entry.name : undefined,
|
|
workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
|
|
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
|
model:
|
|
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
|
? entry.model
|
|
: undefined,
|
|
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
|
|
memorySearch: entry.memorySearch,
|
|
humanDelay: entry.humanDelay,
|
|
heartbeat: entry.heartbeat,
|
|
identity: entry.identity,
|
|
groupChat: entry.groupChat,
|
|
subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined,
|
|
sandbox: entry.sandbox,
|
|
tools: entry.tools,
|
|
};
|
|
}
|
|
|
|
export function resolveAgentSkillsFilter(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string[] | undefined {
|
|
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
|
|
}
|
|
|
|
function resolveModelPrimary(raw: unknown): string | undefined {
|
|
if (typeof raw === "string") {
|
|
const trimmed = raw.trim();
|
|
return trimmed || undefined;
|
|
}
|
|
if (!raw || typeof raw !== "object") {
|
|
return undefined;
|
|
}
|
|
const primary = (raw as { primary?: unknown }).primary;
|
|
if (typeof primary !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = primary.trim();
|
|
return trimmed || undefined;
|
|
}
|
|
|
|
export function resolveAgentExplicitModelPrimary(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string | undefined {
|
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
|
return resolveModelPrimary(raw);
|
|
}
|
|
|
|
export function resolveAgentEffectiveModelPrimary(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string | undefined {
|
|
return (
|
|
resolveAgentExplicitModelPrimary(cfg, agentId) ??
|
|
resolveModelPrimary(cfg.agents?.defaults?.model)
|
|
);
|
|
}
|
|
|
|
// Backward-compatible alias. Prefer explicit/effective helpers at new call sites.
|
|
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
|
return resolveAgentExplicitModelPrimary(cfg, agentId);
|
|
}
|
|
|
|
export function resolveAgentModelFallbacksOverride(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string[] | undefined {
|
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
|
if (!raw || typeof raw === "string") {
|
|
return undefined;
|
|
}
|
|
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
|
if (!Object.hasOwn(raw, "fallbacks")) {
|
|
return undefined;
|
|
}
|
|
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
|
}
|
|
|
|
export function resolveEffectiveModelFallbacks(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
hasSessionModelOverride: boolean;
|
|
}): string[] | undefined {
|
|
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
|
|
if (!params.hasSessionModelOverride) {
|
|
return agentFallbacksOverride;
|
|
}
|
|
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
|
|
return agentFallbacksOverride ?? defaultFallbacks;
|
|
}
|
|
|
|
export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
|
|
const id = normalizeAgentId(agentId);
|
|
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
|
if (configured) {
|
|
return stripNullBytes(resolveUserPath(configured));
|
|
}
|
|
const defaultAgentId = resolveDefaultAgentId(cfg);
|
|
if (id === defaultAgentId) {
|
|
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
if (fallback) {
|
|
return stripNullBytes(resolveUserPath(fallback));
|
|
}
|
|
return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env));
|
|
}
|
|
const stateDir = resolveStateDir(process.env);
|
|
return stripNullBytes(path.join(stateDir, `workspace-${id}`));
|
|
}
|
|
|
|
export function resolveAgentDir(cfg: OpenClawConfig, agentId: string) {
|
|
const id = normalizeAgentId(agentId);
|
|
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
|
|
if (configured) {
|
|
return resolveUserPath(configured);
|
|
}
|
|
const root = resolveStateDir(process.env);
|
|
return path.join(root, "agents", id, "agent");
|
|
}
|