mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 00:20:43 +00:00
* refactor: extract filesystem safety primitives * refactor: use fs-safe for file access helpers * refactor: reuse fs-safe for media reads * refactor: use fs-safe for image reads * refactor: reuse fs-safe in qqbot media opener * refactor: reuse fs-safe for local media checks * refactor: consume cleaner fs-safe api * refactor: align fs-safe json option names * fix: preserve fs-safe migration contracts * refactor: use fs-safe primitive subpaths * refactor: use grouped fs-safe subpaths * refactor: align fs-safe api usage * refactor: adapt private state store api * chore: refresh proof gate * refactor: follow fs-safe json api split * refactor: follow reduced fs-safe surface * build: default fs-safe python helper off * fix: preserve fs-safe plugin sdk aliases * refactor: consolidate fs-safe usage * refactor: unify fs-safe store usage * refactor: trim fs-safe temp workspace usage * refactor: hide low-level fs-safe primitives * build: use published fs-safe package * fix: preserve outbound recovery durability after rebase * chore: refresh pr checks
277 lines
8.9 KiB
TypeScript
277 lines
8.9 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
|
|
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
|
import type { AgentModelConfig } from "../config/types.agents-shared.js";
|
|
import type { AgentConfig } from "../config/types.agents.js";
|
|
import type { OpenClawConfig } from "../config/types.js";
|
|
import { isPathInside } from "../infra/path-guards.js";
|
|
import {
|
|
normalizeAgentId,
|
|
parseAgentSessionKey,
|
|
resolveAgentIdFromSessionKey,
|
|
} from "../routing/session-key.js";
|
|
import {
|
|
lowercasePreservingWhitespace,
|
|
normalizeLowercaseStringOrEmpty,
|
|
normalizeOptionalString,
|
|
resolvePrimaryStringValue,
|
|
} from "../shared/string-coerce.js";
|
|
import { resolveUserPath } from "../utils.js";
|
|
import {
|
|
listAgentEntries,
|
|
listAgentIds,
|
|
resolveAgentConfig,
|
|
resolveAgentContextLimits,
|
|
resolveAgentDir,
|
|
resolveDefaultAgentDir,
|
|
resolveAgentWorkspaceDir,
|
|
resolveDefaultAgentId,
|
|
type ResolvedAgentConfig,
|
|
} from "./agent-scope-config.js";
|
|
import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js";
|
|
export {
|
|
listAgentEntries,
|
|
listAgentIds,
|
|
resolveAgentConfig,
|
|
resolveAgentContextLimits,
|
|
resolveAgentDir,
|
|
resolveDefaultAgentDir,
|
|
resolveAgentWorkspaceDir,
|
|
resolveDefaultAgentId,
|
|
type ResolvedAgentConfig,
|
|
} from "./agent-scope-config.js";
|
|
|
|
/** 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 };
|
|
|
|
export function resolveSessionAgentIds(params: {
|
|
sessionKey?: string;
|
|
config?: OpenClawConfig;
|
|
agentId?: string;
|
|
}): {
|
|
defaultAgentId: string;
|
|
sessionAgentId: string;
|
|
} {
|
|
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
|
const explicitAgentIdRaw = normalizeLowercaseStringOrEmpty(params.agentId);
|
|
const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null;
|
|
const sessionKey = params.sessionKey?.trim();
|
|
const normalizedSessionKey = sessionKey ? normalizeLowercaseStringOrEmpty(sessionKey) : 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;
|
|
}
|
|
|
|
export function resolveAgentExecutionContract(
|
|
cfg: OpenClawConfig | undefined,
|
|
agentId?: string | null,
|
|
): NonNullable<NonNullable<AgentDefaultsConfig["embeddedPi"]>["executionContract"]> | undefined {
|
|
const defaultContract = cfg?.agents?.defaults?.embeddedPi?.executionContract;
|
|
if (!cfg || !agentId) {
|
|
return defaultContract;
|
|
}
|
|
const agentContract = resolveAgentConfig(cfg, agentId)?.embeddedPi?.executionContract;
|
|
return agentContract ?? defaultContract;
|
|
}
|
|
|
|
export function resolveAgentSkillsFilter(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string[] | undefined {
|
|
return resolveEffectiveAgentSkillFilter(cfg, agentId);
|
|
}
|
|
|
|
export function resolveAgentExplicitModelPrimary(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string | undefined {
|
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
|
return resolvePrimaryStringValue(raw);
|
|
}
|
|
|
|
export function resolveAgentEffectiveModelPrimary(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
): string | undefined {
|
|
return (
|
|
resolveAgentExplicitModelPrimary(cfg, agentId) ??
|
|
resolvePrimaryStringValue(cfg.agents?.defaults?.model)
|
|
);
|
|
}
|
|
|
|
function findMutableAgentEntry(cfg: OpenClawConfig, agentId: string): AgentConfig | undefined {
|
|
const id = normalizeAgentId(agentId);
|
|
return cfg.agents?.list?.find((entry) => normalizeAgentId(entry?.id) === id);
|
|
}
|
|
|
|
function updateAgentModelPrimary(
|
|
existing: AgentModelConfig | undefined,
|
|
primary: string,
|
|
): AgentModelConfig {
|
|
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
return { ...existing, primary };
|
|
}
|
|
return primary;
|
|
}
|
|
|
|
export type AgentModelPrimaryWriteTarget = "agent" | "defaults";
|
|
|
|
export function setAgentEffectiveModelPrimary(
|
|
cfg: OpenClawConfig,
|
|
agentId: string,
|
|
primary: string,
|
|
): AgentModelPrimaryWriteTarget {
|
|
const id = normalizeAgentId(agentId);
|
|
if (resolveAgentExplicitModelPrimary(cfg, id)) {
|
|
const entry = findMutableAgentEntry(cfg, id);
|
|
if (entry) {
|
|
entry.model = updateAgentModelPrimary(entry.model, primary);
|
|
return "agent";
|
|
}
|
|
}
|
|
cfg.agents ??= {};
|
|
cfg.agents.defaults ??= {};
|
|
cfg.agents.defaults.model = updateAgentModelPrimary(cfg.agents.defaults.model, primary);
|
|
return "defaults";
|
|
}
|
|
|
|
/** @deprecated 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) {
|
|
return undefined;
|
|
}
|
|
if (typeof raw === "string") {
|
|
return resolvePrimaryStringValue(raw) ? [] : undefined;
|
|
}
|
|
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
|
if (!Object.hasOwn(raw, "fallbacks")) {
|
|
return Object.hasOwn(raw, "primary") && resolvePrimaryStringValue(raw) ? [] : undefined;
|
|
}
|
|
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
|
}
|
|
|
|
export function resolveFallbackAgentId(params: {
|
|
agentId?: string | null;
|
|
sessionKey?: string | null;
|
|
}): string {
|
|
const explicitAgentId = normalizeOptionalString(params.agentId) ?? "";
|
|
if (explicitAgentId) {
|
|
return normalizeAgentId(explicitAgentId);
|
|
}
|
|
return resolveAgentIdFromSessionKey(params.sessionKey);
|
|
}
|
|
|
|
export function resolveRunModelFallbacksOverride(params: {
|
|
cfg: OpenClawConfig | undefined;
|
|
agentId?: string | null;
|
|
sessionKey?: string | null;
|
|
}): string[] | undefined {
|
|
if (!params.cfg) {
|
|
return undefined;
|
|
}
|
|
return resolveAgentModelFallbacksOverride(
|
|
params.cfg,
|
|
resolveFallbackAgentId({ agentId: params.agentId, sessionKey: params.sessionKey }),
|
|
);
|
|
}
|
|
|
|
export function hasConfiguredModelFallbacks(params: {
|
|
cfg: OpenClawConfig | undefined;
|
|
agentId?: string | null;
|
|
sessionKey?: string | null;
|
|
}): boolean {
|
|
const fallbacksOverride = resolveRunModelFallbacksOverride(params);
|
|
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.model);
|
|
return (fallbacksOverride ?? defaultFallbacks).length > 0;
|
|
}
|
|
|
|
export function resolveEffectiveModelFallbacks(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
hasSessionModelOverride: boolean;
|
|
modelOverrideSource?: "auto" | "user";
|
|
}): string[] | undefined {
|
|
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
|
|
if (!params.hasSessionModelOverride) {
|
|
return agentFallbacksOverride;
|
|
}
|
|
if (params.modelOverrideSource !== "auto") {
|
|
return [];
|
|
}
|
|
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
|
|
return agentFallbacksOverride ?? defaultFallbacks;
|
|
}
|
|
|
|
function normalizePathForComparison(input: string): string {
|
|
const resolved = path.resolve(stripNullBytes(resolveUserPath(input)));
|
|
let normalized = resolved;
|
|
// Prefer realpath when available to normalize aliases/symlinks (for example /tmp -> /private/tmp)
|
|
// and canonical path case without forcing case-folding on case-sensitive macOS volumes.
|
|
try {
|
|
normalized = fs.realpathSync.native(resolved);
|
|
} catch {
|
|
// Keep lexical path for non-existent directories.
|
|
}
|
|
if (process.platform === "win32") {
|
|
return lowercasePreservingWhitespace(normalized);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function resolveAgentIdsByWorkspacePath(
|
|
cfg: OpenClawConfig,
|
|
workspacePath: string,
|
|
): string[] {
|
|
const normalizedWorkspacePath = normalizePathForComparison(workspacePath);
|
|
const ids = listAgentIds(cfg);
|
|
const matches: Array<{ id: string; workspaceDir: string; order: number }> = [];
|
|
|
|
for (let index = 0; index < ids.length; index += 1) {
|
|
const id = ids[index];
|
|
const workspaceDir = normalizePathForComparison(resolveAgentWorkspaceDir(cfg, id));
|
|
if (!isPathInside(workspaceDir, normalizedWorkspacePath)) {
|
|
continue;
|
|
}
|
|
matches.push({ id, workspaceDir, order: index });
|
|
}
|
|
|
|
matches.sort((left, right) => {
|
|
const workspaceLengthDelta = right.workspaceDir.length - left.workspaceDir.length;
|
|
if (workspaceLengthDelta !== 0) {
|
|
return workspaceLengthDelta;
|
|
}
|
|
return left.order - right.order;
|
|
});
|
|
|
|
return matches.map((entry) => entry.id);
|
|
}
|
|
|
|
export function resolveAgentIdByWorkspacePath(
|
|
cfg: OpenClawConfig,
|
|
workspacePath: string,
|
|
): string | undefined {
|
|
return resolveAgentIdsByWorkspacePath(cfg, workspacePath)[0];
|
|
}
|