mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 12:11:23 +00:00
feat: wire multi-agent config and routing
Co-authored-by: Mark Pors <1078320+pors@users.noreply.github.com>
This commit is contained in:
@@ -11,10 +11,8 @@ describe("resolveAgentConfig", () => {
|
||||
|
||||
it("should return undefined when agent id does not exist", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
main: { workspace: "~/clawd" },
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", workspace: "~/clawd" }],
|
||||
},
|
||||
};
|
||||
const result = resolveAgentConfig(cfg, "nonexistent");
|
||||
@@ -23,15 +21,16 @@ describe("resolveAgentConfig", () => {
|
||||
|
||||
it("should return basic agent config", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
name: "Main Agent",
|
||||
workspace: "~/clawd",
|
||||
agentDir: "~/.clawdbot/agents/main",
|
||||
model: "anthropic/claude-opus-4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = resolveAgentConfig(cfg, "main");
|
||||
@@ -40,6 +39,9 @@ describe("resolveAgentConfig", () => {
|
||||
workspace: "~/clawd",
|
||||
agentDir: "~/.clawdbot/agents/main",
|
||||
model: "anthropic/claude-opus-4",
|
||||
identity: undefined,
|
||||
groupChat: undefined,
|
||||
subagents: undefined,
|
||||
sandbox: undefined,
|
||||
tools: undefined,
|
||||
});
|
||||
@@ -47,9 +49,10 @@ describe("resolveAgentConfig", () => {
|
||||
|
||||
it("should return agent-specific sandbox config", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -57,13 +60,9 @@ describe("resolveAgentConfig", () => {
|
||||
perSession: false,
|
||||
workspaceAccess: "ro",
|
||||
workspaceRoot: "~/sandboxes",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = resolveAgentConfig(cfg, "work");
|
||||
@@ -73,25 +72,22 @@ describe("resolveAgentConfig", () => {
|
||||
perSession: false,
|
||||
workspaceAccess: "ro",
|
||||
workspaceRoot: "~/sandboxes",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return agent-specific tools config", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
restricted: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "restricted",
|
||||
workspace: "~/clawd-restricted",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash", "write", "edit"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = resolveAgentConfig(cfg, "restricted");
|
||||
@@ -103,9 +99,10 @@ describe("resolveAgentConfig", () => {
|
||||
|
||||
it("should return both sandbox and tools config", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
family: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "family",
|
||||
workspace: "~/clawd-family",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -116,7 +113,7 @@ describe("resolveAgentConfig", () => {
|
||||
deny: ["bash"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = resolveAgentConfig(cfg, "family");
|
||||
@@ -126,10 +123,8 @@ describe("resolveAgentConfig", () => {
|
||||
|
||||
it("should normalize agent id", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
main: { workspace: "~/clawd" },
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", workspace: "~/clawd" }],
|
||||
},
|
||||
};
|
||||
// Should normalize to "main" (default)
|
||||
|
||||
@@ -11,6 +11,24 @@ import {
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
|
||||
|
||||
type AgentEntry = NonNullable<
|
||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||
>[number];
|
||||
|
||||
type ResolvedAgentConfig = {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
identity?: AgentEntry["identity"];
|
||||
groupChat?: AgentEntry["groupChat"];
|
||||
subagents?: AgentEntry["subagents"];
|
||||
sandbox?: AgentEntry["sandbox"];
|
||||
tools?: AgentEntry["tools"];
|
||||
};
|
||||
|
||||
let defaultAgentWarned = false;
|
||||
|
||||
export function resolveAgentIdFromSessionKey(
|
||||
sessionKey?: string | null,
|
||||
): string {
|
||||
@@ -18,46 +36,51 @@ export function resolveAgentIdFromSessionKey(
|
||||
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
||||
}
|
||||
|
||||
function listAgents(cfg: ClawdbotConfig): 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 resolveDefaultAgentId(cfg: ClawdbotConfig): string {
|
||||
const agents = listAgents(cfg);
|
||||
if (agents.length === 0) return DEFAULT_AGENT_ID;
|
||||
const defaults = agents.filter((agent) => agent?.default);
|
||||
if (defaults.length > 1 && !defaultAgentWarned) {
|
||||
defaultAgentWarned = true;
|
||||
console.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);
|
||||
}
|
||||
|
||||
function resolveAgentEntry(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
): AgentEntry | undefined {
|
||||
const id = normalizeAgentId(agentId);
|
||||
return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
||||
}
|
||||
|
||||
export function resolveAgentConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
):
|
||||
| {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
subagents?: {
|
||||
allowAgents?: string[];
|
||||
};
|
||||
sandbox?: {
|
||||
mode?: "off" | "non-main" | "all";
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
scope?: "session" | "agent" | "shared";
|
||||
perSession?: boolean;
|
||||
workspaceRoot?: string;
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
};
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
}
|
||||
| undefined {
|
||||
): ResolvedAgentConfig | undefined {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const agents = cfg.routing?.agents;
|
||||
if (!agents || typeof agents !== "object") return undefined;
|
||||
const entry = agents[id];
|
||||
if (!entry || typeof entry !== "object") return undefined;
|
||||
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 : undefined,
|
||||
identity: entry.identity,
|
||||
groupChat: entry.groupChat,
|
||||
subagents:
|
||||
typeof entry.subagents === "object" && entry.subagents
|
||||
? entry.subagents
|
||||
@@ -71,9 +94,10 @@ export function resolveAgentWorkspaceDir(cfg: ClawdbotConfig, agentId: string) {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||
if (configured) return resolveUserPath(configured);
|
||||
if (id === DEFAULT_AGENT_ID) {
|
||||
const legacy = cfg.agent?.workspace?.trim();
|
||||
if (legacy) return resolveUserPath(legacy);
|
||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||
if (id === defaultAgentId) {
|
||||
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
||||
if (fallback) return resolveUserPath(fallback);
|
||||
return DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
}
|
||||
return path.join(os.homedir(), `clawd-${id}`);
|
||||
|
||||
@@ -925,7 +925,10 @@ export function resolveAuthProfileOrder(params: {
|
||||
|
||||
// Still put preferredProfile first if specified
|
||||
if (preferredProfile && ordered.includes(preferredProfile)) {
|
||||
return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)];
|
||||
return [
|
||||
preferredProfile,
|
||||
...ordered.filter((e) => e !== preferredProfile),
|
||||
];
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ function formatUserTime(date: Date, timeZone: string): string | undefined {
|
||||
}
|
||||
|
||||
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||
const models = cfg?.agent?.models ?? {};
|
||||
const models = cfg?.agents?.defaults?.models ?? {};
|
||||
const entries: Array<{ alias: string; model: string }> = [];
|
||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||
const model = String(keyRaw ?? "").trim();
|
||||
@@ -134,7 +134,9 @@ function buildSystemPrompt(params: {
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
modelDisplay: string;
|
||||
}) {
|
||||
const userTimezone = resolveUserTimezone(params.config?.agent?.userTimezone);
|
||||
const userTimezone = resolveUserTimezone(
|
||||
params.config?.agents?.defaults?.userTimezone,
|
||||
);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
return buildAgentSystemPrompt({
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -143,7 +145,7 @@ function buildSystemPrompt(params: {
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
reasoningTagHint: false,
|
||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||
params.config?.agent?.heartbeat?.prompt,
|
||||
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||
),
|
||||
runtimeInfo: {
|
||||
host: "clawdbot",
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("gateway tool", () => {
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing gateway tool");
|
||||
|
||||
const raw = '{\n agent: { workspace: "~/clawd" }\n}\n';
|
||||
const raw = '{\n agents: { defaults: { workspace: "~/clawd" } }\n}\n';
|
||||
await tool.execute("call2", {
|
||||
action: "config.apply",
|
||||
raw,
|
||||
|
||||
@@ -52,18 +52,20 @@ describe("agents_list", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
name: "Main",
|
||||
subagents: {
|
||||
allowAgents: ["research"],
|
||||
},
|
||||
},
|
||||
research: {
|
||||
{
|
||||
id: "research",
|
||||
name: "Research",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -87,20 +89,23 @@ describe("agents_list", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["*"],
|
||||
},
|
||||
},
|
||||
research: {
|
||||
{
|
||||
id: "research",
|
||||
name: "Research",
|
||||
},
|
||||
coder: {
|
||||
{
|
||||
id: "coder",
|
||||
name: "Coder",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -131,14 +136,15 @@ describe("agents_list", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["research"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -314,14 +314,15 @@ describe("subagents", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["beta"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -365,14 +366,15 @@ describe("subagents", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["*"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -416,14 +418,15 @@ describe("subagents", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["Research"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -467,14 +470,15 @@ describe("subagents", () => {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["alpha"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ function buildAllowedModelKeys(
|
||||
defaultProvider: string,
|
||||
): Set<string> | null {
|
||||
const rawAllowlist = (() => {
|
||||
const modelMap = cfg?.agent?.models ?? {};
|
||||
const modelMap = cfg?.agents?.defaults?.models ?? {};
|
||||
return Object.keys(modelMap);
|
||||
})();
|
||||
if (rawAllowlist.length === 0) return null;
|
||||
@@ -85,7 +85,7 @@ function resolveImageFallbackCandidates(params: {
|
||||
if (params.modelOverride?.trim()) {
|
||||
addRaw(params.modelOverride, false);
|
||||
} else {
|
||||
const imageModel = params.cfg?.agent?.imageModel as
|
||||
const imageModel = params.cfg?.agents?.defaults?.imageModel as
|
||||
| { primary?: string }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -95,7 +95,7 @@ function resolveImageFallbackCandidates(params: {
|
||||
}
|
||||
|
||||
const imageFallbacks = (() => {
|
||||
const imageModel = params.cfg?.agent?.imageModel as
|
||||
const imageModel = params.cfg?.agents?.defaults?.imageModel as
|
||||
| { fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -142,7 +142,7 @@ function resolveFallbackCandidates(params: {
|
||||
addCandidate({ provider, model }, false);
|
||||
|
||||
const modelFallbacks = (() => {
|
||||
const model = params.cfg?.agent?.model as
|
||||
const model = params.cfg?.agents?.defaults?.model as
|
||||
| { fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -253,7 +253,7 @@ export async function runWithImageModelFallback<T>(params: {
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(
|
||||
"No image model configured. Set agent.imageModel.primary or agent.imageModel.fallbacks.",
|
||||
"No image model configured. Set agents.defaults.imageModel.primary or agents.defaults.imageModel.fallbacks.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ const catalog = [
|
||||
describe("buildAllowedModelSet", () => {
|
||||
it("always allows the configured default model", () => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
models: {
|
||||
"openai/gpt-4": { alias: "gpt4" },
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": { alias: "gpt4" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
@@ -41,7 +43,7 @@ describe("buildAllowedModelSet", () => {
|
||||
|
||||
it("includes the default model when no allowlist is set", () => {
|
||||
const cfg = {
|
||||
agent: {},
|
||||
agents: { defaults: {} },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const allowed = buildAllowedModelSet({
|
||||
|
||||
@@ -65,7 +65,7 @@ export function buildModelAliasIndex(params: {
|
||||
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
|
||||
const byKey = new Map<string, string[]>();
|
||||
|
||||
const rawModels = params.cfg.agent?.models ?? {};
|
||||
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
||||
if (!parsed) continue;
|
||||
@@ -109,7 +109,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
defaultModel: string;
|
||||
}): ModelRef {
|
||||
const rawModel = (() => {
|
||||
const raw = params.cfg.agent?.model as
|
||||
const raw = params.cfg.agents?.defaults?.model as
|
||||
| { primary?: string }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -128,7 +128,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
aliasIndex,
|
||||
});
|
||||
if (resolved) return resolved.ref;
|
||||
// TODO(steipete): drop this fallback once provider-less agent.model is fully deprecated.
|
||||
// TODO(steipete): drop this fallback once provider-less agents.defaults.model is fully deprecated.
|
||||
return { provider: "anthropic", model: trimmed };
|
||||
}
|
||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
||||
@@ -145,7 +145,7 @@ export function buildAllowedModelSet(params: {
|
||||
allowedKeys: Set<string>;
|
||||
} {
|
||||
const rawAllowlist = (() => {
|
||||
const modelMap = params.cfg.agent?.models ?? {};
|
||||
const modelMap = params.cfg.agents?.defaults?.models ?? {};
|
||||
return Object.keys(modelMap);
|
||||
})();
|
||||
const allowAny = rawAllowlist.length === 0;
|
||||
@@ -203,7 +203,7 @@ export function resolveThinkingDefault(params: {
|
||||
model: string;
|
||||
catalog?: ModelCatalogEntry[];
|
||||
}): ThinkLevel {
|
||||
const configured = params.cfg.agent?.thinkingDefault;
|
||||
const configured = params.cfg.agents?.defaults?.thinkingDefault;
|
||||
if (configured) return configured;
|
||||
const candidate = params.catalog?.find(
|
||||
(entry) => entry.provider === params.provider && entry.id === params.model,
|
||||
|
||||
@@ -110,12 +110,14 @@ describe("resolveExtraParams", () => {
|
||||
it("respects explicit thinking config from user (disable thinking)", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: {
|
||||
type: "disabled",
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: {
|
||||
type: "disabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -136,12 +138,14 @@ describe("resolveExtraParams", () => {
|
||||
it("preserves other params while adding thinking config", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -164,13 +168,15 @@ describe("resolveExtraParams", () => {
|
||||
it("does not override explicit thinking config even if partial", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
// User explicitly omitted clear_thinking
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
// User explicitly omitted clear_thinking
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -214,12 +220,14 @@ describe("resolveExtraParams", () => {
|
||||
it("passes through params for non-GLM models without modification", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"openai/gpt-4": {
|
||||
params: {
|
||||
logprobs: true,
|
||||
top_logprobs: 5,
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": {
|
||||
params: {
|
||||
logprobs: true,
|
||||
top_logprobs: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -264,7 +272,7 @@ describe("resolveExtraParams", () => {
|
||||
|
||||
it("handles config with empty models gracefully", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: { agent: { models: {} } },
|
||||
cfg: { agents: { defaults: { models: {} } } },
|
||||
provider: "zai",
|
||||
modelId: "glm-4.7",
|
||||
});
|
||||
@@ -280,12 +288,14 @@ describe("resolveExtraParams", () => {
|
||||
it("model alias lookup uses exact provider/model key", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
alias: "smart",
|
||||
params: {
|
||||
custom_param: "value",
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
alias: "smart",
|
||||
params: {
|
||||
custom_param: "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -307,11 +317,13 @@ describe("resolveExtraParams", () => {
|
||||
it("treats thinking: null as explicit config (no auto-enable)", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: null,
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
thinking: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -374,11 +386,13 @@ describe("resolveExtraParams", () => {
|
||||
it("thinkLevel: 'off' still passes through explicit config", () => {
|
||||
const result = resolveExtraParams({
|
||||
cfg: {
|
||||
agent: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
custom_param: "value",
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-4.7": {
|
||||
params: {
|
||||
custom_param: "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -105,7 +105,7 @@ import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
* - GLM-4.5/4.6: Interleaved thinking (clear_thinking: true) - reasoning cleared each turn
|
||||
*
|
||||
* Users can override via config:
|
||||
* agent.models["zai/glm-4.7"].params.thinking = { type: "disabled" }
|
||||
* agents.defaults.models["zai/glm-4.7"].params.thinking = { type: "disabled" }
|
||||
*
|
||||
* Or disable via runtime flag: --thinking off
|
||||
*
|
||||
@@ -119,7 +119,7 @@ export function resolveExtraParams(params: {
|
||||
thinkLevel?: string;
|
||||
}): Record<string, unknown> | undefined {
|
||||
const modelKey = `${params.provider}/${params.modelId}`;
|
||||
const modelConfig = params.cfg?.agent?.models?.[modelKey];
|
||||
const modelConfig = params.cfg?.agents?.defaults?.models?.[modelKey];
|
||||
let extraParams = modelConfig?.params ? { ...modelConfig.params } : undefined;
|
||||
|
||||
// Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured
|
||||
@@ -200,10 +200,10 @@ function resolveContextWindowTokens(params: {
|
||||
if (fromModelsConfig) return fromModelsConfig;
|
||||
|
||||
const fromAgentConfig =
|
||||
typeof params.cfg?.agent?.contextTokens === "number" &&
|
||||
Number.isFinite(params.cfg.agent.contextTokens) &&
|
||||
params.cfg.agent.contextTokens > 0
|
||||
? Math.floor(params.cfg.agent.contextTokens)
|
||||
typeof params.cfg?.agents?.defaults?.contextTokens === "number" &&
|
||||
Number.isFinite(params.cfg.agents.defaults.contextTokens) &&
|
||||
params.cfg.agents.defaults.contextTokens > 0
|
||||
? Math.floor(params.cfg.agents.defaults.contextTokens)
|
||||
: undefined;
|
||||
if (fromAgentConfig) return fromAgentConfig;
|
||||
|
||||
@@ -217,7 +217,7 @@ function buildContextPruningExtension(params: {
|
||||
modelId: string;
|
||||
model: Model<Api> | undefined;
|
||||
}): { additionalExtensionPaths?: string[] } {
|
||||
const raw = params.cfg?.agent?.contextPruning;
|
||||
const raw = params.cfg?.agents?.defaults?.contextPruning;
|
||||
if (raw?.mode !== "adaptive" && raw?.mode !== "aggressive") return {};
|
||||
|
||||
const settings = computeEffectiveSettings(raw);
|
||||
@@ -254,7 +254,7 @@ export type EmbeddedPiRunMeta = {
|
||||
};
|
||||
|
||||
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||
const models = cfg?.agent?.models ?? {};
|
||||
const models = cfg?.agents?.defaults?.models ?? {};
|
||||
const entries: Array<{ alias: string; model: string }> = [];
|
||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||
const model = String(keyRaw ?? "").trim();
|
||||
@@ -844,7 +844,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||
const tools = createClawdbotCodingTools({
|
||||
bash: {
|
||||
...params.config?.agent?.bash,
|
||||
...params.config?.tools?.bash,
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
@@ -865,7 +865,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
||||
const reasoningTagHint = provider === "ollama";
|
||||
const userTimezone = resolveUserTimezone(
|
||||
params.config?.agent?.userTimezone,
|
||||
params.config?.agents?.defaults?.userTimezone,
|
||||
);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||
@@ -875,7 +875,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||
params.config?.agent?.heartbeat?.prompt,
|
||||
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||
),
|
||||
runtimeInfo,
|
||||
sandboxInfo,
|
||||
@@ -1157,7 +1157,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||
const tools = createClawdbotCodingTools({
|
||||
bash: {
|
||||
...params.config?.agent?.bash,
|
||||
...params.config?.tools?.bash,
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
@@ -1178,7 +1178,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
||||
const reasoningTagHint = provider === "ollama";
|
||||
const userTimezone = resolveUserTimezone(
|
||||
params.config?.agent?.userTimezone,
|
||||
params.config?.agents?.defaults?.userTimezone,
|
||||
);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||
@@ -1188,7 +1188,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||
params.config?.agent?.heartbeat?.prompt,
|
||||
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||
),
|
||||
runtimeInfo,
|
||||
sandboxInfo,
|
||||
@@ -1444,7 +1444,8 @@ export async function runEmbeddedPiAgent(params: {
|
||||
}
|
||||
|
||||
const fallbackConfigured =
|
||||
(params.config?.agent?.model?.fallbacks?.length ?? 0) > 0;
|
||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) >
|
||||
0;
|
||||
const authFailure = isAuthAssistantError(lastAssistant);
|
||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||
|
||||
|
||||
@@ -6,18 +6,17 @@ import type { SandboxDockerConfig } from "./sandbox.js";
|
||||
describe("Agent-specific tool filtering", () => {
|
||||
it("should apply global tool policy when no agent-specific policy exists", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
tools: {
|
||||
allow: ["read", "write"],
|
||||
deny: ["bash"],
|
||||
},
|
||||
tools: {
|
||||
allow: ["read", "write"],
|
||||
deny: ["bash"],
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "~/clawd",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,22 +35,21 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
it("should apply agent-specific tool policy", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"],
|
||||
deny: [],
|
||||
},
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"],
|
||||
deny: [],
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
restricted: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "restricted",
|
||||
workspace: "~/clawd-restricted",
|
||||
tools: {
|
||||
allow: ["read"], // Agent override: only read
|
||||
deny: ["bash", "write", "edit"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,20 +69,22 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
it("should allow different tool policies for different agents", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "~/clawd",
|
||||
// No tools restriction - all tools available
|
||||
},
|
||||
family: {
|
||||
{
|
||||
id: "family",
|
||||
workspace: "~/clawd-family",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash", "write", "edit", "process"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,20 +116,19 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
it("should prefer agent-specific tool policy over global", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
tools: {
|
||||
deny: ["browser"], // Global deny
|
||||
},
|
||||
tools: {
|
||||
deny: ["browser"], // Global deny
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
tools: {
|
||||
deny: ["bash", "process"], // Agent deny (override)
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -149,19 +148,16 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
it("should work with sandbox tools filtering", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"], // Sandbox allows these
|
||||
deny: [],
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
restricted: {
|
||||
list: [
|
||||
{
|
||||
id: "restricted",
|
||||
workspace: "~/clawd-restricted",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -172,6 +168,14 @@ describe("Agent-specific tool filtering", () => {
|
||||
deny: ["bash", "write"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"], // Sandbox allows these
|
||||
deny: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -216,10 +220,8 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
it("should run bash synchronously when process is denied", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
tools: {
|
||||
deny: ["process"],
|
||||
},
|
||||
tools: {
|
||||
deny: ["process"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
sessionKey: "agent:main:subagent:test",
|
||||
// Intentionally partial config; only fields used by pi-tools are provided.
|
||||
config: {
|
||||
agent: {
|
||||
tools: {
|
||||
subagents: {
|
||||
tools: {
|
||||
// Policy matching is case-insensitive
|
||||
@@ -325,7 +325,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
|
||||
it("filters tools by agent tool policy even without sandbox", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: { agent: { tools: { deny: ["browser"] } } },
|
||||
config: { tools: { deny: ["browser"] } },
|
||||
});
|
||||
// NOTE: bash is capitalized to bypass Anthropic OAuth blocking
|
||||
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
||||
|
||||
@@ -429,7 +429,7 @@ const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||
];
|
||||
|
||||
function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPolicy {
|
||||
const configured = cfg?.agent?.subagents?.tools;
|
||||
const configured = cfg?.tools?.subagents?.tools;
|
||||
const deny = [
|
||||
...DEFAULT_SUBAGENT_TOOL_DENY,
|
||||
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
||||
@@ -466,7 +466,7 @@ function resolveEffectiveToolPolicy(params: {
|
||||
? resolveAgentConfig(params.config, agentId)
|
||||
: undefined;
|
||||
const hasAgentTools = agentConfig?.tools !== undefined;
|
||||
const globalTools = params.config?.agent?.tools;
|
||||
const globalTools = params.config?.tools;
|
||||
return {
|
||||
agentId,
|
||||
policy: hasAgentTools ? agentConfig?.tools : globalTools,
|
||||
|
||||
@@ -56,18 +56,19 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
workspace: "~/clawd",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "~/clawd",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,18 +86,19 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
docker: {
|
||||
setupCommand: "echo global",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
docker: {
|
||||
setupCommand: "echo global",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -106,7 +108,7 @@ describe("Agent-specific sandbox config", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -133,18 +135,19 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
docker: {
|
||||
setupCommand: "echo global",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
docker: {
|
||||
setupCommand: "echo global",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -154,7 +157,7 @@ describe("Agent-specific sandbox config", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -182,19 +185,20 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
docker: {
|
||||
image: "global-image",
|
||||
network: "none",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
docker: {
|
||||
image: "global-image",
|
||||
network: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -205,7 +209,7 @@ describe("Agent-specific sandbox config", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -224,21 +228,22 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all", // Global default
|
||||
scope: "agent",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all", // Global default
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "~/clawd",
|
||||
sandbox: {
|
||||
mode: "off", // Agent override
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -256,21 +261,22 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "off", // Global default
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "off", // Global default
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
family: {
|
||||
list: [
|
||||
{
|
||||
id: "family",
|
||||
workspace: "~/clawd-family",
|
||||
sandbox: {
|
||||
mode: "all", // Agent override
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -288,22 +294,23 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "session", // Global default
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "session", // Global default
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent", // Agent override
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -322,16 +329,17 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
workspaceRoot: "~/.clawdbot/sandboxes", // Global default
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
workspaceRoot: "~/.clawdbot/sandboxes", // Global default
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
isolated: {
|
||||
list: [
|
||||
{
|
||||
id: "isolated",
|
||||
workspace: "~/clawd-isolated",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -339,7 +347,7 @@ describe("Agent-specific sandbox config", () => {
|
||||
workspaceRoot: "/tmp/isolated-sandboxes", // Agent override
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -359,28 +367,30 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
scope: "session",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
scope: "session",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
main: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "~/clawd",
|
||||
sandbox: {
|
||||
mode: "off", // main: no sandbox
|
||||
},
|
||||
},
|
||||
family: {
|
||||
{
|
||||
id: "family",
|
||||
workspace: "~/clawd-family",
|
||||
sandbox: {
|
||||
mode: "all", // family: always sandbox
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -406,29 +416,38 @@ describe("Agent-specific sandbox config", () => {
|
||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
restricted: {
|
||||
list: [
|
||||
{
|
||||
id: "restricted",
|
||||
workspace: "~/clawd-restricted",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
tools: {
|
||||
allow: ["read", "write"],
|
||||
deny: ["edit"],
|
||||
},
|
||||
tools: {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["read", "write"],
|
||||
deny: ["edit"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "./agent-scope.js";
|
||||
import { syncSkillsToWorkspace } from "./skills.js";
|
||||
import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
@@ -345,15 +348,14 @@ export function resolveSandboxConfigForAgent(
|
||||
cfg?: ClawdbotConfig,
|
||||
agentId?: string,
|
||||
): SandboxConfig {
|
||||
const agent = cfg?.agent?.sandbox;
|
||||
const agent = cfg?.agents?.defaults?.sandbox;
|
||||
|
||||
// Agent-specific sandbox config overrides global
|
||||
let agentSandbox: typeof agent | undefined;
|
||||
if (agentId && cfg?.routing?.agents) {
|
||||
const agentConfig = cfg.routing.agents[agentId];
|
||||
if (agentConfig && typeof agentConfig === "object") {
|
||||
agentSandbox = agentConfig.sandbox;
|
||||
}
|
||||
const agentConfig =
|
||||
cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||
if (agentConfig?.sandbox) {
|
||||
agentSandbox = agentConfig.sandbox;
|
||||
}
|
||||
|
||||
const scope = resolveSandboxScope({
|
||||
@@ -382,9 +384,13 @@ export function resolveSandboxConfigForAgent(
|
||||
}),
|
||||
tools: {
|
||||
allow:
|
||||
agentSandbox?.tools?.allow ?? agent?.tools?.allow ?? DEFAULT_TOOL_ALLOW,
|
||||
agentConfig?.tools?.sandbox?.tools?.allow ??
|
||||
cfg?.tools?.sandbox?.tools?.allow ??
|
||||
DEFAULT_TOOL_ALLOW,
|
||||
deny:
|
||||
agentSandbox?.tools?.deny ?? agent?.tools?.deny ?? DEFAULT_TOOL_DENY,
|
||||
agentConfig?.tools?.sandbox?.tools?.deny ??
|
||||
cfg?.tools?.sandbox?.tools?.deny ??
|
||||
DEFAULT_TOOL_DENY,
|
||||
},
|
||||
prune: resolveSandboxPruneConfig({
|
||||
scope,
|
||||
@@ -1059,7 +1065,7 @@ export async function resolveSandboxContext(params: {
|
||||
await ensureSandboxWorkspace(
|
||||
sandboxWorkspaceDir,
|
||||
agentWorkspaceDir,
|
||||
params.config?.agent?.skipBootstrap,
|
||||
params.config?.agents?.defaults?.skipBootstrap,
|
||||
);
|
||||
if (cfg.workspaceAccess === "none") {
|
||||
try {
|
||||
@@ -1133,7 +1139,7 @@ export async function ensureSandboxWorkspaceForSession(params: {
|
||||
await ensureSandboxWorkspace(
|
||||
sandboxWorkspaceDir,
|
||||
agentWorkspaceDir,
|
||||
params.config?.agent?.skipBootstrap,
|
||||
params.config?.agents?.defaults?.skipBootstrap,
|
||||
);
|
||||
if (cfg.workspaceAccess === "none") {
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,7 @@ let listenerStarted = false;
|
||||
|
||||
function resolveArchiveAfterMs() {
|
||||
const cfg = loadConfig();
|
||||
const minutes = cfg.agent?.subagents?.archiveAfterMinutes ?? 60;
|
||||
const minutes = cfg.agents?.defaults?.subagents?.archiveAfterMinutes ?? 60;
|
||||
if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
|
||||
return Math.max(1, Math.floor(minutes)) * 60_000;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const normalizeNumber = (value: unknown): number | undefined =>
|
||||
: undefined;
|
||||
|
||||
export function resolveAgentTimeoutSeconds(cfg?: ClawdbotConfig): number {
|
||||
const raw = normalizeNumber(cfg?.agent?.timeoutSeconds);
|
||||
const raw = normalizeNumber(cfg?.agents?.defaults?.timeoutSeconds);
|
||||
const seconds = raw ?? DEFAULT_AGENT_TIMEOUT_SECONDS;
|
||||
return Math.max(seconds, 1);
|
||||
}
|
||||
|
||||
@@ -55,19 +55,17 @@ export function createAgentsListTool(opts?: {
|
||||
.map((value) => normalizeAgentId(value)),
|
||||
);
|
||||
|
||||
const configuredAgents = cfg.routing?.agents ?? {};
|
||||
const configuredIds = Object.keys(configuredAgents).map((key) =>
|
||||
normalizeAgentId(key),
|
||||
const configuredAgents = Array.isArray(cfg.agents?.list)
|
||||
? cfg.agents?.list
|
||||
: [];
|
||||
const configuredIds = configuredAgents.map((entry) =>
|
||||
normalizeAgentId(entry.id),
|
||||
);
|
||||
const configuredNameMap = new Map<string, string>();
|
||||
for (const [key, value] of Object.entries(configuredAgents)) {
|
||||
if (!value || typeof value !== "object") continue;
|
||||
const name =
|
||||
typeof (value as { name?: unknown }).name === "string"
|
||||
? ((value as { name?: string }).name?.trim() ?? "")
|
||||
: "";
|
||||
for (const entry of configuredAgents) {
|
||||
const name = entry?.name?.trim() ?? "";
|
||||
if (!name) continue;
|
||||
configuredNameMap.set(normalizeAgentId(key), name);
|
||||
configuredNameMap.set(normalizeAgentId(entry.id), name);
|
||||
}
|
||||
|
||||
const allowed = new Set<string>();
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { AnyAgentTool } from "./common.js";
|
||||
const DEFAULT_PROMPT = "Describe the image.";
|
||||
|
||||
function ensureImageToolConfigured(cfg?: ClawdbotConfig): boolean {
|
||||
const imageModel = cfg?.agent?.imageModel as
|
||||
const imageModel = cfg?.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -45,7 +45,7 @@ function pickMaxBytes(
|
||||
) {
|
||||
return Math.floor(maxBytesMb * 1024 * 1024);
|
||||
}
|
||||
const configured = cfg?.agent?.mediaMaxMb;
|
||||
const configured = cfg?.agents?.defaults?.mediaMaxMb;
|
||||
if (
|
||||
typeof configured === "number" &&
|
||||
Number.isFinite(configured) &&
|
||||
@@ -141,7 +141,7 @@ export function createImageTool(options?: {
|
||||
label: "Image",
|
||||
name: "image",
|
||||
description:
|
||||
"Analyze an image with the configured image model (agent.imageModel). Provide a prompt and image path or URL.",
|
||||
"Analyze an image with the configured image model (agents.defaults.imageModel). Provide a prompt and image path or URL.",
|
||||
parameters: Type.Object({
|
||||
prompt: Type.Optional(Type.String()),
|
||||
image: Type.String(),
|
||||
|
||||
@@ -25,7 +25,7 @@ const SessionsHistoryToolSchema = Type.Object({
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
async function isSpawnedSessionAllowed(params: {
|
||||
@@ -97,7 +97,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||
}
|
||||
}
|
||||
|
||||
const routingA2A = cfg.routing?.agentToAgent;
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
@@ -126,14 +126,13 @@ export function createSessionsHistoryTool(opts?: {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent history is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent access.",
|
||||
"Agent-to-agent history is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access.",
|
||||
});
|
||||
}
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent history denied by routing.agentToAgent.allow.",
|
||||
error: "Agent-to-agent history denied by tools.agentToAgent.allow.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
loadConfig: () =>
|
||||
({
|
||||
session: { scope: "per-sender", mainKey: "main" },
|
||||
routing: { agentToAgent: { enabled: false } },
|
||||
tools: { agentToAgent: { enabled: false } },
|
||||
}) as never,
|
||||
};
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("sessions_list gating", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("filters out other agents when routing.agentToAgent.enabled is false", async () => {
|
||||
it("filters out other agents when tools.agentToAgent.enabled is false", async () => {
|
||||
const tool = createSessionsListTool({ agentSessionKey: "agent:main:main" });
|
||||
const result = await tool.execute("call1", {});
|
||||
expect(result.details).toMatchObject({
|
||||
|
||||
@@ -53,7 +53,7 @@ const SessionsListToolSchema = Type.Object({
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
export function createSessionsListTool(opts?: {
|
||||
@@ -126,7 +126,7 @@ export function createSessionsListTool(opts?: {
|
||||
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
||||
const routingA2A = cfg.routing?.agentToAgent;
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
|
||||
@@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
loadConfig: () =>
|
||||
({
|
||||
session: { scope: "per-sender", mainKey: "main" },
|
||||
routing: { agentToAgent: { enabled: false } },
|
||||
tools: { agentToAgent: { enabled: false } },
|
||||
}) as never,
|
||||
};
|
||||
});
|
||||
@@ -25,7 +25,7 @@ describe("sessions_send gating", () => {
|
||||
callGatewayMock.mockReset();
|
||||
});
|
||||
|
||||
it("blocks cross-agent sends when routing.agentToAgent.enabled is false", async () => {
|
||||
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
|
||||
const tool = createSessionsSendTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentProvider: "whatsapp",
|
||||
|
||||
@@ -54,7 +54,7 @@ export function createSessionsSendTool(opts?: {
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility =
|
||||
cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
const requesterInternalKey =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
@@ -126,7 +126,7 @@ export function createSessionsSendTool(opts?: {
|
||||
mainKey,
|
||||
});
|
||||
|
||||
const routingA2A = cfg.routing?.agentToAgent;
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
@@ -156,7 +156,7 @@ export function createSessionsSendTool(opts?: {
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent messaging is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent sends.",
|
||||
"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export function createSessionsSendTool(opts?: {
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent messaging denied by routing.agentToAgent.allow.",
|
||||
"Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user