mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 13:01:42 +03: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)
|
||||
|
||||
+57
-33
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+17
-11
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,9 +85,11 @@ describe("block streaming", () => {
|
||||
onBlockReply,
|
||||
},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -140,9 +142,11 @@ describe("block streaming", () => {
|
||||
onBlockReply,
|
||||
},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
telegram: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -185,9 +189,11 @@ describe("block streaming", () => {
|
||||
onBlockReply,
|
||||
},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -239,9 +245,11 @@ describe("block streaming", () => {
|
||||
blockReplyTimeoutMs: 10,
|
||||
},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
telegram: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
|
||||
@@ -78,11 +78,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: " help " },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: " help " },
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
@@ -108,9 +110,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -138,11 +142,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
messages: {
|
||||
queue: {
|
||||
mode: "collect",
|
||||
debounceMs: 1500,
|
||||
@@ -174,10 +180,12 @@ describe("directive behavior", () => {
|
||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
thinkingDefault: "high",
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -198,9 +206,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -232,9 +242,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -270,9 +282,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -303,9 +317,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -330,9 +346,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/verbose on", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -352,10 +370,12 @@ describe("directive behavior", () => {
|
||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
thinkingDefault: "high",
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -376,9 +396,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -399,10 +421,12 @@ describe("directive behavior", () => {
|
||||
{ Body: "/verbose", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
verboseDefault: "on",
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
verboseDefault: "on",
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -423,9 +447,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/reasoning", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
@@ -452,10 +478,14 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
elevatedDefault: "on",
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
elevatedDefault: "on",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
@@ -486,13 +516,17 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
@@ -520,9 +554,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
@@ -552,9 +590,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
@@ -585,9 +627,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
@@ -613,9 +659,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: storePath },
|
||||
@@ -644,9 +692,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: storePath },
|
||||
@@ -677,9 +727,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: storePath },
|
||||
@@ -690,9 +742,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/queue reset", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: storePath },
|
||||
@@ -749,9 +803,11 @@ describe("directive behavior", () => {
|
||||
ctx,
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -810,9 +866,11 @@ describe("directive behavior", () => {
|
||||
{ Body: "/verbose on", From: ctx.From, To: ctx.To },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -825,9 +883,11 @@ describe("directive behavior", () => {
|
||||
ctx,
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -853,12 +913,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -883,12 +945,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model status", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -913,12 +977,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model list", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -943,12 +1009,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -972,11 +1040,13 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model list", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -999,12 +1069,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -1030,12 +1102,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -1081,12 +1155,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -1112,12 +1188,14 @@ describe("directive behavior", () => {
|
||||
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -1151,12 +1229,14 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
@@ -1204,9 +1284,11 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -1242,9 +1324,13 @@ describe("directive behavior", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1004"] },
|
||||
},
|
||||
|
||||
@@ -57,9 +57,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
|
||||
function makeCfg(home: string) {
|
||||
return {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
|
||||
@@ -53,9 +53,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
|
||||
function makeCfg(home: string) {
|
||||
return {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
|
||||
@@ -50,13 +50,15 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
|
||||
function makeCfg(home: string, queue?: Record<string, unknown>) {
|
||||
return {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
routing: queue ? { queue } : undefined,
|
||||
messages: queue ? { queue } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,18 @@ const usageMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
||||
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
compactEmbeddedPiSession,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveAgentIdFromSessionKey,
|
||||
resolveSessionKey,
|
||||
} from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
@@ -61,9 +66,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
|
||||
function makeCfg(home: string) {
|
||||
return {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -345,9 +352,11 @@ describe("trigger handling", () => {
|
||||
it("allows owner to set send policy", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
@@ -381,9 +390,13 @@ describe("trigger handling", () => {
|
||||
it("allows approved sender to toggle elevated mode", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -420,9 +433,13 @@ describe("trigger handling", () => {
|
||||
it("rejects elevated toggles when disabled", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
enabled: false,
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
@@ -467,9 +484,13 @@ describe("trigger handling", () => {
|
||||
},
|
||||
});
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -510,9 +531,13 @@ describe("trigger handling", () => {
|
||||
},
|
||||
});
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -545,9 +570,13 @@ describe("trigger handling", () => {
|
||||
it("allows elevated directive in groups when mentioned", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -589,9 +618,13 @@ describe("trigger handling", () => {
|
||||
it("allows elevated directive in direct chats without mentions", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -635,9 +668,13 @@ describe("trigger handling", () => {
|
||||
},
|
||||
});
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
@@ -668,9 +705,11 @@ describe("trigger handling", () => {
|
||||
it("falls back to discord dm allowFrom for elevated approval", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
dm: {
|
||||
@@ -708,9 +747,13 @@ describe("trigger handling", () => {
|
||||
it("treats explicit discord elevated allowlist as override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
elevated: {
|
||||
allowFrom: { discord: [] },
|
||||
},
|
||||
@@ -799,9 +842,12 @@ describe("trigger handling", () => {
|
||||
});
|
||||
|
||||
const cfg = makeCfg(home);
|
||||
cfg.agent = {
|
||||
...cfg.agent,
|
||||
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
||||
},
|
||||
};
|
||||
|
||||
await getReplyFromConfig(
|
||||
@@ -941,15 +987,17 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
routing: {
|
||||
messages: {
|
||||
groupChat: {},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
@@ -985,9 +1033,11 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -1024,9 +1074,11 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -1056,9 +1108,11 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
@@ -1083,9 +1137,11 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
@@ -1124,9 +1180,11 @@ describe("trigger handling", () => {
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
@@ -1229,12 +1287,14 @@ describe("trigger handling", () => {
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
sandbox: {
|
||||
mode: "non-main" as const,
|
||||
workspaceRoot: join(home, "sandboxes"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
sandbox: {
|
||||
mode: "non-main" as const,
|
||||
workspaceRoot: join(home, "sandboxes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
@@ -1272,10 +1332,11 @@ describe("trigger handling", () => {
|
||||
ctx,
|
||||
cfg.session?.mainKey,
|
||||
);
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const sandbox = await ensureSandboxWorkspaceForSession({
|
||||
config: cfg,
|
||||
sessionKey,
|
||||
workspaceDir: cfg.agent.workspace,
|
||||
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
|
||||
});
|
||||
expect(sandbox).not.toBeNull();
|
||||
if (!sandbox) {
|
||||
|
||||
@@ -212,7 +212,7 @@ export async function getReplyFromConfig(
|
||||
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
||||
const cfg = configOverride ?? loadConfig();
|
||||
const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey);
|
||||
const agentCfg = cfg.agent;
|
||||
const agentCfg = cfg.agents?.defaults;
|
||||
const sessionCfg = cfg.session;
|
||||
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
|
||||
cfg,
|
||||
@@ -239,7 +239,7 @@ export async function getReplyFromConfig(
|
||||
resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
@@ -257,7 +257,7 @@ export async function getReplyFromConfig(
|
||||
opts?.onTypingController?.(typing);
|
||||
|
||||
let transcribedText: string | undefined;
|
||||
if (cfg.routing?.transcribeAudio && isAudio(ctx.MediaType)) {
|
||||
if (cfg.audio?.transcription && isAudio(ctx.MediaType)) {
|
||||
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
|
||||
if (transcribed?.text) {
|
||||
transcribedText = transcribed.text;
|
||||
@@ -329,7 +329,7 @@ export async function getReplyFromConfig(
|
||||
cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
|
||||
),
|
||||
);
|
||||
const configuredAliases = Object.values(cfg.agent?.models ?? {})
|
||||
const configuredAliases = Object.values(cfg.agents?.defaults?.models ?? {})
|
||||
.map((entry) => entry.alias?.trim())
|
||||
.filter((alias): alias is string => Boolean(alias))
|
||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||
@@ -391,7 +391,7 @@ export async function getReplyFromConfig(
|
||||
sessionCtx.Provider?.trim().toLowerCase() ??
|
||||
ctx.Provider?.trim().toLowerCase() ??
|
||||
"";
|
||||
const elevatedConfig = agentCfg?.elevated;
|
||||
const elevatedConfig = cfg.tools?.elevated;
|
||||
const discordElevatedFallback =
|
||||
messageProviderKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
|
||||
const elevatedEnabled = elevatedConfig?.enabled !== false;
|
||||
|
||||
@@ -34,7 +34,7 @@ export function resolveBlockStreamingChunking(
|
||||
} {
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey);
|
||||
const chunkCfg = cfg?.agent?.blockStreamingChunk;
|
||||
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||
const maxRequested = Math.max(
|
||||
1,
|
||||
Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX),
|
||||
|
||||
@@ -163,18 +163,19 @@ export async function buildStatusReply(params: {
|
||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
||||
defaultGroupActivation())
|
||||
: undefined;
|
||||
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||
const statusText = buildStatusMessage({
|
||||
config: cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
...agentDefaults,
|
||||
model: {
|
||||
...cfg.agent?.model,
|
||||
...agentDefaults.model,
|
||||
primary: `${provider}/${model}`,
|
||||
},
|
||||
contextTokens,
|
||||
thinkingDefault: cfg.agent?.thinkingDefault,
|
||||
verboseDefault: cfg.agent?.verboseDefault,
|
||||
elevatedDefault: cfg.agent?.elevatedDefault,
|
||||
thinkingDefault: agentDefaults.thinkingDefault,
|
||||
verboseDefault: agentDefaults.verboseDefault,
|
||||
elevatedDefault: agentDefaults.elevatedDefault,
|
||||
},
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
resolveConfiguredModelRef,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
@@ -363,16 +364,16 @@ export async function handleDirectiveOnly(params: {
|
||||
currentElevatedLevel,
|
||||
} = params;
|
||||
const runtimeIsSandboxed = (() => {
|
||||
const sandboxMode = params.cfg.agent?.sandbox?.mode ?? "off";
|
||||
if (sandboxMode === "off") return false;
|
||||
const sessionKey = params.sessionKey?.trim();
|
||||
if (!sessionKey) return false;
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
|
||||
if (sandboxCfg.mode === "off") return false;
|
||||
const mainKey = resolveAgentMainSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
});
|
||||
if (sandboxMode === "all") return true;
|
||||
if (sandboxCfg.mode === "all") return true;
|
||||
return sessionKey !== mainKey;
|
||||
})();
|
||||
const shouldHintDirectRuntime =
|
||||
@@ -394,7 +395,9 @@ export async function handleDirectiveOnly(params: {
|
||||
provider: string;
|
||||
id: string;
|
||||
}> = [];
|
||||
for (const raw of Object.keys(params.cfg.agent?.models ?? {})) {
|
||||
for (const raw of Object.keys(
|
||||
params.cfg.agents?.defaults?.models ?? {},
|
||||
)) {
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: String(raw),
|
||||
defaultProvider,
|
||||
@@ -851,7 +854,7 @@ export async function persistInlineDirectives(params: {
|
||||
model: string;
|
||||
initialModelLabel: string;
|
||||
formatModelSwitchEvent: (label: string, alias?: string) => string;
|
||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
||||
agentCfg: NonNullable<ClawdbotConfig["agents"]>["defaults"] | undefined;
|
||||
}): Promise<{ provider: string; model: string; contextTokens: number }> {
|
||||
const {
|
||||
directives,
|
||||
@@ -1007,13 +1010,16 @@ export function resolveDefaultModel(params: {
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
? {
|
||||
...params.cfg,
|
||||
agent: {
|
||||
...params.cfg.agent,
|
||||
model: {
|
||||
...(typeof params.cfg.agent?.model === "object"
|
||||
? params.cfg.agent.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
agents: {
|
||||
...params.cfg.agents,
|
||||
defaults: {
|
||||
...params.cfg.agents?.defaults,
|
||||
model: {
|
||||
...(typeof params.cfg.agents?.defaults?.model === "object"
|
||||
? params.cfg.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
describe("mention helpers", () => {
|
||||
it("builds regexes and skips invalid patterns", () => {
|
||||
const regexes = buildMentionRegexes({
|
||||
routing: {
|
||||
messages: {
|
||||
groupChat: { mentionPatterns: ["\\bclawd\\b", "(invalid"] },
|
||||
},
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe("mention helpers", () => {
|
||||
|
||||
it("matches patterns case-insensitively", () => {
|
||||
const regexes = buildMentionRegexes({
|
||||
routing: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
|
||||
});
|
||||
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
|
||||
});
|
||||
@@ -31,11 +31,16 @@ describe("mention helpers", () => {
|
||||
it("uses per-agent mention patterns when configured", () => {
|
||||
const regexes = buildMentionRegexes(
|
||||
{
|
||||
routing: {
|
||||
messages: {
|
||||
groupChat: { mentionPatterns: ["\\bglobal\\b"] },
|
||||
agents: {
|
||||
work: { mentionPatterns: ["\\bworkbot\\b"] },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
groupChat: { mentionPatterns: ["\\bworkbot\\b"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"work",
|
||||
|
||||
@@ -1,23 +1,62 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
function escapeRegExp(text: string): string {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
|
||||
const patterns: string[] = [];
|
||||
const name = identity?.name?.trim();
|
||||
if (name) {
|
||||
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
|
||||
const re = parts.length ? parts.join(String.raw`\s+`) : escapeRegExp(name);
|
||||
patterns.push(String.raw`\b@?${re}\b`);
|
||||
}
|
||||
const emoji = identity?.emoji?.trim();
|
||||
if (emoji) {
|
||||
patterns.push(escapeRegExp(emoji));
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
const BACKSPACE_CHAR = "\u0008";
|
||||
|
||||
function normalizeMentionPattern(pattern: string): string {
|
||||
if (!pattern.includes(BACKSPACE_CHAR)) return pattern;
|
||||
return pattern.split(BACKSPACE_CHAR).join("\\b");
|
||||
}
|
||||
|
||||
function normalizeMentionPatterns(patterns: string[]): string[] {
|
||||
return patterns.map(normalizeMentionPattern);
|
||||
}
|
||||
|
||||
function resolveMentionPatterns(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
agentId?: string,
|
||||
): string[] {
|
||||
if (!cfg) return [];
|
||||
const agentConfig = agentId ? cfg.routing?.agents?.[agentId] : undefined;
|
||||
if (agentConfig && Object.hasOwn(agentConfig, "mentionPatterns")) {
|
||||
return agentConfig.mentionPatterns ?? [];
|
||||
const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||
const agentGroupChat = agentConfig?.groupChat;
|
||||
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
|
||||
return agentGroupChat.mentionPatterns ?? [];
|
||||
}
|
||||
return cfg.routing?.groupChat?.mentionPatterns ?? [];
|
||||
const globalGroupChat = cfg.messages?.groupChat;
|
||||
if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
|
||||
return globalGroupChat.mentionPatterns ?? [];
|
||||
}
|
||||
const derived = deriveMentionPatterns(agentConfig?.identity);
|
||||
return derived.length > 0 ? derived : [];
|
||||
}
|
||||
|
||||
export function buildMentionRegexes(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
agentId?: string,
|
||||
): RegExp[] {
|
||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
||||
const patterns = normalizeMentionPatterns(
|
||||
resolveMentionPatterns(cfg, agentId),
|
||||
);
|
||||
return patterns
|
||||
.map((pattern) => {
|
||||
try {
|
||||
@@ -66,7 +105,9 @@ export function stripMentions(
|
||||
agentId?: string,
|
||||
): string {
|
||||
let result = text;
|
||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
||||
const patterns = normalizeMentionPatterns(
|
||||
resolveMentionPatterns(cfg, agentId),
|
||||
);
|
||||
for (const p of patterns) {
|
||||
try {
|
||||
const re = new RegExp(p, "gi");
|
||||
|
||||
@@ -33,7 +33,9 @@ type ModelSelectionState = {
|
||||
|
||||
export async function createModelSelectionState(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
||||
agentCfg:
|
||||
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
| undefined;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
@@ -201,7 +203,9 @@ export function resolveModelDirectiveSelection(params: {
|
||||
}
|
||||
|
||||
export function resolveContextTokens(params: {
|
||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
||||
agentCfg:
|
||||
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
| undefined;
|
||||
model: string;
|
||||
}): number {
|
||||
return (
|
||||
|
||||
@@ -553,7 +553,7 @@ export function resolveQueueSettings(params: {
|
||||
inlineOptions?: Partial<QueueSettings>;
|
||||
}): QueueSettings {
|
||||
const providerKey = params.provider?.trim().toLowerCase();
|
||||
const queueCfg = params.cfg.routing?.queue;
|
||||
const queueCfg = params.cfg.messages?.queue;
|
||||
const providerModeRaw =
|
||||
providerKey && queueCfg?.byProvider
|
||||
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
|
||||
|
||||
@@ -35,7 +35,9 @@ import type {
|
||||
VerboseLevel,
|
||||
} from "./thinking.js";
|
||||
|
||||
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
||||
type AgentConfig = Partial<
|
||||
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
>;
|
||||
|
||||
export const formatTokenCount = formatTokenCountShared;
|
||||
|
||||
@@ -188,7 +190,11 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
const now = args.now ?? Date.now();
|
||||
const entry = args.sessionEntry;
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg: { agent: args.agent ?? {} },
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: args.agent ?? {},
|
||||
},
|
||||
} as ClawdbotConfig,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
@@ -37,8 +37,8 @@ describe("transcribeInboundAudio", () => {
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const cfg = {
|
||||
routing: {
|
||||
transcribeAudio: {
|
||||
audio: {
|
||||
transcription: {
|
||||
command: ["echo", "{{MediaPath}}"],
|
||||
timeoutSeconds: 5,
|
||||
},
|
||||
@@ -64,7 +64,7 @@ describe("transcribeInboundAudio", () => {
|
||||
it("returns undefined when no transcription command", async () => {
|
||||
const { transcribeInboundAudio } = await import("./transcription.js");
|
||||
const res = await transcribeInboundAudio(
|
||||
{ routing: {} } as never,
|
||||
{ audio: {} } as never,
|
||||
{} as never,
|
||||
runtime as never,
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function transcribeInboundAudio(
|
||||
ctx: MsgContext,
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<{ text: string } | undefined> {
|
||||
const transcriber = cfg.routing?.transcribeAudio;
|
||||
const transcriber = cfg.audio?.transcription;
|
||||
if (!transcriber?.command?.length) return undefined;
|
||||
|
||||
const timeoutMs = Math.max((transcriber.timeoutSeconds ?? 45) * 1000, 1_000);
|
||||
|
||||
@@ -267,10 +267,14 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--no-probe", "Skip live probes; list free candidates only")
|
||||
.option("--yes", "Accept defaults without prompting", false)
|
||||
.option("--no-input", "Disable prompts (use defaults)")
|
||||
.option("--set-default", "Set agent.model to the first selection", false)
|
||||
.option(
|
||||
"--set-default",
|
||||
"Set agents.defaults.model to the first selection",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--set-image",
|
||||
"Set agent.imageModel to the first image selection",
|
||||
"Set agents.defaults.imageModel to the first image selection",
|
||||
false,
|
||||
)
|
||||
.option("--json", "Output JSON", false)
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ export function buildProgram() {
|
||||
.description("Initialize ~/.clawdbot/clawdbot.json and the agent workspace")
|
||||
.option(
|
||||
"--workspace <dir>",
|
||||
"Agent workspace directory (default: ~/clawd; stored as agent.workspace)",
|
||||
"Agent workspace directory (default: ~/clawd; stored as agents.defaults.workspace)",
|
||||
)
|
||||
.option("--wizard", "Run the interactive onboarding wizard", false)
|
||||
.option("--non-interactive", "Run the wizard without prompts", false)
|
||||
@@ -1163,7 +1163,7 @@ Examples:
|
||||
clawdbot sessions --json # machine-readable output
|
||||
clawdbot sessions --store ./tmp/sessions.json
|
||||
|
||||
Shows token usage per session when the agent reports it; set agent.contextTokens to see % of your model window.`,
|
||||
Shows token usage per session when the agent reports it; set agents.defaults.contextTokens to see % of your model window.`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
|
||||
+20
-4
@@ -1,5 +1,9 @@
|
||||
import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import {
|
||||
buildWorkspaceSkillStatus,
|
||||
type SkillStatusEntry,
|
||||
@@ -363,7 +367,10 @@ export function registerSkillsCli(program: Command) {
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = config.agent?.workspace ?? process.cwd();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
config,
|
||||
resolveDefaultAgentId(config),
|
||||
);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, { config });
|
||||
console.log(formatSkillsList(report, opts));
|
||||
} catch (err) {
|
||||
@@ -380,7 +387,10 @@ export function registerSkillsCli(program: Command) {
|
||||
.action(async (name, opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = config.agent?.workspace ?? process.cwd();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
config,
|
||||
resolveDefaultAgentId(config),
|
||||
);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, { config });
|
||||
console.log(formatSkillInfo(report, name, opts));
|
||||
} catch (err) {
|
||||
@@ -396,7 +406,10 @@ export function registerSkillsCli(program: Command) {
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = config.agent?.workspace ?? process.cwd();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
config,
|
||||
resolveDefaultAgentId(config),
|
||||
);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, { config });
|
||||
console.log(formatSkillsCheck(report, opts));
|
||||
} catch (err) {
|
||||
@@ -409,7 +422,10 @@ export function registerSkillsCli(program: Command) {
|
||||
skills.action(async () => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = config.agent?.workspace ?? process.cwd();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
config,
|
||||
resolveDefaultAgentId(config),
|
||||
);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, { config });
|
||||
console.log(formatSkillsList(report, {}));
|
||||
} catch (err) {
|
||||
|
||||
@@ -29,9 +29,11 @@ const configSpy = vi.spyOn(configModule, "loadConfig");
|
||||
|
||||
function mockConfig(storePath: string, overrides?: Partial<ClawdbotConfig>) {
|
||||
configSpy.mockReturnValue({
|
||||
agent: {
|
||||
timeoutSeconds: 600,
|
||||
...overrides?.agent,
|
||||
agents: {
|
||||
defaults: {
|
||||
timeoutSeconds: 600,
|
||||
...overrides?.agents?.defaults,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
|
||||
@@ -80,7 +80,7 @@ function parseTimeoutSeconds(opts: {
|
||||
const raw =
|
||||
opts.timeout !== undefined
|
||||
? Number.parseInt(String(opts.timeout), 10)
|
||||
: (opts.cfg.agent?.timeoutSeconds ?? 600);
|
||||
: (opts.cfg.agents?.defaults?.timeoutSeconds ?? 600);
|
||||
if (Number.isNaN(raw) || raw <= 0) {
|
||||
throw new Error("--timeout must be a positive integer (seconds)");
|
||||
}
|
||||
|
||||
+18
-12
@@ -53,19 +53,21 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
function mockConfig(
|
||||
home: string,
|
||||
storePath: string,
|
||||
routingOverrides?: Partial<NonNullable<ClawdbotConfig["routing"]>>,
|
||||
agentOverrides?: Partial<NonNullable<ClawdbotConfig["agent"]>>,
|
||||
agentOverrides?: Partial<
|
||||
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||
>,
|
||||
telegramOverrides?: Partial<NonNullable<ClawdbotConfig["telegram"]>>,
|
||||
) {
|
||||
configSpy.mockReturnValue({
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
models: { "anthropic/claude-opus-4-5": {} },
|
||||
workspace: path.join(home, "clawd"),
|
||||
...agentOverrides,
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
models: { "anthropic/claude-opus-4-5": {} },
|
||||
workspace: path.join(home, "clawd"),
|
||||
...agentOverrides,
|
||||
},
|
||||
},
|
||||
session: { store: storePath, mainKey: "main" },
|
||||
routing: routingOverrides ? { ...routingOverrides } : undefined,
|
||||
telegram: telegramOverrides ? { ...telegramOverrides } : undefined,
|
||||
});
|
||||
}
|
||||
@@ -153,11 +155,15 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses provider/model from agent.model", async () => {
|
||||
it("uses provider/model from agents.defaults.model.primary", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, undefined, {
|
||||
model: "openai/gpt-4.1-mini",
|
||||
mockConfig(home, store, {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
});
|
||||
|
||||
await agentCommand({ message: "hi", to: "+1555" }, runtime);
|
||||
@@ -269,7 +275,7 @@ describe("agentCommand", () => {
|
||||
it("passes through telegram accountId when delivering", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, undefined, undefined, { botToken: "t-1" });
|
||||
mockConfig(home, store, undefined, { botToken: "t-1" });
|
||||
const deps = {
|
||||
sendMessageWhatsApp: vi.fn(),
|
||||
sendMessageTelegram: vi
|
||||
|
||||
@@ -181,13 +181,13 @@ export async function agentCommand(
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const agentCfg = cfg.agent;
|
||||
const agentCfg = cfg.agents?.defaults;
|
||||
const sessionAgentId = resolveAgentIdFromSessionKey(opts.sessionKey?.trim());
|
||||
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, sessionAgentId);
|
||||
const agentDir = resolveAgentDir(cfg, sessionAgentId);
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
|
||||
|
||||
+49
-44
@@ -1,9 +1,9 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||
import {
|
||||
applyAgentBindings,
|
||||
applyAgentConfig,
|
||||
@@ -12,27 +12,32 @@ import {
|
||||
} from "./agents.js";
|
||||
|
||||
describe("agents helpers", () => {
|
||||
it("buildAgentSummaries includes default + routing agents", () => {
|
||||
it("buildAgentSummaries includes default + configured agents", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { workspace: "/main-ws", model: { primary: "anthropic/claude" } },
|
||||
routing: {
|
||||
defaultAgentId: "work",
|
||||
agents: {
|
||||
work: {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/main-ws",
|
||||
model: { primary: "anthropic/claude" },
|
||||
},
|
||||
list: [
|
||||
{ id: "main" },
|
||||
{
|
||||
id: "work",
|
||||
default: true,
|
||||
name: "Work",
|
||||
workspace: "/work-ws",
|
||||
agentDir: "/state/agents/work/agent",
|
||||
model: "openai/gpt-4.1",
|
||||
},
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "biz" },
|
||||
},
|
||||
{ agentId: "main", match: { provider: "telegram" } },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "work",
|
||||
match: { provider: "whatsapp", accountId: "biz" },
|
||||
},
|
||||
{ agentId: "main", match: { provider: "telegram" } },
|
||||
],
|
||||
};
|
||||
|
||||
const summaries = buildAgentSummaries(cfg);
|
||||
@@ -40,7 +45,7 @@ describe("agents helpers", () => {
|
||||
const work = summaries.find((summary) => summary.id === "work");
|
||||
|
||||
expect(main).toBeTruthy();
|
||||
expect(main?.workspace).toBe(path.resolve("/main-ws"));
|
||||
expect(main?.workspace).toBe(path.join(os.homedir(), "clawd-main"));
|
||||
expect(main?.bindings).toBe(1);
|
||||
expect(main?.model).toBe("anthropic/claude");
|
||||
expect(main?.agentDir.endsWith(path.join("agents", "main", "agent"))).toBe(
|
||||
@@ -57,10 +62,8 @@ describe("agents helpers", () => {
|
||||
|
||||
it("applyAgentConfig merges updates", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
agents: {
|
||||
work: { workspace: "/old-ws", model: "anthropic/claude" },
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "work", workspace: "/old-ws", model: "anthropic/claude" }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,7 +74,7 @@ describe("agents helpers", () => {
|
||||
agentDir: "/state/work/agent",
|
||||
});
|
||||
|
||||
const work = next.routing?.agents?.work;
|
||||
const work = next.agents?.list?.find((agent) => agent.id === "work");
|
||||
expect(work?.name).toBe("Work");
|
||||
expect(work?.workspace).toBe("/new-ws");
|
||||
expect(work?.agentDir).toBe("/state/work/agent");
|
||||
@@ -80,14 +83,12 @@ describe("agents helpers", () => {
|
||||
|
||||
it("applyAgentBindings skips duplicates and reports conflicts", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { provider: "whatsapp", accountId: "default" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyAgentBindings(cfg, [
|
||||
@@ -108,32 +109,36 @@ describe("agents helpers", () => {
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.skipped).toHaveLength(1);
|
||||
expect(result.conflicts).toHaveLength(1);
|
||||
expect(result.config.routing?.bindings).toHaveLength(2);
|
||||
expect(result.config.bindings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("pruneAgentConfig removes agent, bindings, and allowlist entries", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
routing: {
|
||||
defaultAgentId: "work",
|
||||
agents: {
|
||||
work: { workspace: "/work-ws" },
|
||||
home: { workspace: "/home-ws" },
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "work", match: { provider: "whatsapp" } },
|
||||
{ agentId: "home", match: { provider: "telegram" } },
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "work", default: true, workspace: "/work-ws" },
|
||||
{ id: "home", workspace: "/home-ws" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "work", match: { provider: "whatsapp" } },
|
||||
{ agentId: "home", match: { provider: "telegram" } },
|
||||
],
|
||||
tools: {
|
||||
agentToAgent: { enabled: true, allow: ["work", "home"] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = pruneAgentConfig(cfg, "work");
|
||||
expect(result.config.routing?.agents?.work).toBeUndefined();
|
||||
expect(result.config.routing?.agents?.home).toBeTruthy();
|
||||
expect(result.config.routing?.bindings).toHaveLength(1);
|
||||
expect(result.config.routing?.bindings?.[0]?.agentId).toBe("home");
|
||||
expect(result.config.routing?.agentToAgent?.allow).toEqual(["home"]);
|
||||
expect(result.config.routing?.defaultAgentId).toBe(DEFAULT_AGENT_ID);
|
||||
expect(
|
||||
result.config.agents?.list?.some((agent) => agent.id === "work"),
|
||||
).toBe(false);
|
||||
expect(
|
||||
result.config.agents?.list?.some((agent) => agent.id === "home"),
|
||||
).toBe(true);
|
||||
expect(result.config.bindings).toHaveLength(1);
|
||||
expect(result.config.bindings?.[0]?.agentId).toBe("home");
|
||||
expect(result.config.tools?.agentToAgent?.allow).toEqual(["home"]);
|
||||
expect(result.removedBindings).toBe(1);
|
||||
expect(result.removedAllow).toBe(1);
|
||||
});
|
||||
|
||||
+105
-74
@@ -1,9 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js";
|
||||
@@ -114,6 +114,10 @@ type AgentBinding = {
|
||||
};
|
||||
};
|
||||
|
||||
type AgentEntry = NonNullable<
|
||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||
>[number];
|
||||
|
||||
type AgentIdentity = {
|
||||
name?: string;
|
||||
emoji?: string;
|
||||
@@ -140,15 +144,32 @@ function createQuietRuntime(runtime: RuntimeEnv): RuntimeEnv {
|
||||
return { ...runtime, log: () => {} };
|
||||
}
|
||||
|
||||
function listAgentEntries(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"),
|
||||
);
|
||||
}
|
||||
|
||||
function findAgentEntryIndex(list: AgentEntry[], agentId: string): number {
|
||||
const id = normalizeAgentId(agentId);
|
||||
return list.findIndex((entry) => normalizeAgentId(entry.id) === id);
|
||||
}
|
||||
|
||||
function resolveAgentName(cfg: ClawdbotConfig, agentId: string) {
|
||||
return cfg.routing?.agents?.[agentId]?.name?.trim() || undefined;
|
||||
const entry = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
return entry?.name?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) {
|
||||
if (agentId !== DEFAULT_AGENT_ID) {
|
||||
return cfg.routing?.agents?.[agentId]?.model?.trim() || undefined;
|
||||
}
|
||||
const raw = cfg.agent?.model;
|
||||
const entry = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
if (entry?.model?.trim()) return entry.model.trim();
|
||||
const raw = cfg.agents?.defaults?.model;
|
||||
if (typeof raw === "string") return raw;
|
||||
return raw?.primary?.trim() || undefined;
|
||||
}
|
||||
@@ -183,37 +204,33 @@ function loadAgentIdentity(workspace: string): AgentIdentity | null {
|
||||
}
|
||||
|
||||
export function buildAgentSummaries(cfg: ClawdbotConfig): AgentSummary[] {
|
||||
const defaultAgentId = normalizeAgentId(
|
||||
cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
const agentIds = new Set<string>([
|
||||
DEFAULT_AGENT_ID,
|
||||
defaultAgentId,
|
||||
...Object.keys(cfg.routing?.agents ?? {}),
|
||||
]);
|
||||
|
||||
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 cfg.routing?.bindings ?? []) {
|
||||
for (const binding of cfg.bindings ?? []) {
|
||||
const agentId = normalizeAgentId(binding.agentId);
|
||||
bindingCounts.set(agentId, (bindingCounts.get(agentId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const ordered = [
|
||||
DEFAULT_AGENT_ID,
|
||||
...[...agentIds]
|
||||
.filter((id) => id !== DEFAULT_AGENT_ID)
|
||||
.sort((a, b) => a.localeCompare(b)),
|
||||
];
|
||||
const ordered = orderedIds.filter(
|
||||
(id, index) => orderedIds.indexOf(id) === index,
|
||||
);
|
||||
|
||||
return ordered.map((id) => {
|
||||
const workspace = resolveAgentWorkspaceDir(cfg, id);
|
||||
const identity = loadAgentIdentity(workspace);
|
||||
const fallbackIdentity = id === defaultAgentId ? cfg.identity : undefined;
|
||||
const identityName = identity?.name ?? fallbackIdentity?.name?.trim();
|
||||
const identityEmoji = identity?.emoji ?? fallbackIdentity?.emoji?.trim();
|
||||
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 identitySource = identity
|
||||
? "identity"
|
||||
: fallbackIdentity && (identityName || identityEmoji)
|
||||
: configIdentity && (identityName || identityEmoji)
|
||||
? "config"
|
||||
: undefined;
|
||||
return {
|
||||
@@ -242,22 +259,34 @@ export function applyAgentConfig(
|
||||
},
|
||||
): ClawdbotConfig {
|
||||
const agentId = normalizeAgentId(params.agentId);
|
||||
const existing = cfg.routing?.agents?.[agentId] ?? {};
|
||||
const name = params.name?.trim();
|
||||
const list = listAgentEntries(cfg);
|
||||
const index = findAgentEntryIndex(list, agentId);
|
||||
const base = index >= 0 ? list[index] : { id: agentId };
|
||||
const nextEntry: AgentEntry = {
|
||||
...base,
|
||||
...(name ? { name } : {}),
|
||||
...(params.workspace ? { workspace: params.workspace } : {}),
|
||||
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
};
|
||||
const nextList = [...list];
|
||||
if (index >= 0) {
|
||||
nextList[index] = nextEntry;
|
||||
} else {
|
||||
if (
|
||||
nextList.length === 0 &&
|
||||
agentId !== normalizeAgentId(resolveDefaultAgentId(cfg))
|
||||
) {
|
||||
nextList.push({ id: resolveDefaultAgentId(cfg) });
|
||||
}
|
||||
nextList.push(nextEntry);
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
routing: {
|
||||
...cfg.routing,
|
||||
agents: {
|
||||
...cfg.routing?.agents,
|
||||
[agentId]: {
|
||||
...existing,
|
||||
...(name ? { name } : {}),
|
||||
...(params.workspace ? { workspace: params.workspace } : {}),
|
||||
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
list: nextList,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -283,7 +312,7 @@ export function applyAgentBindings(
|
||||
skipped: AgentBinding[];
|
||||
conflicts: Array<{ binding: AgentBinding; existingAgentId: string }>;
|
||||
} {
|
||||
const existing = cfg.routing?.bindings ?? [];
|
||||
const existing = cfg.bindings ?? [];
|
||||
const existingMatchMap = new Map<string, string>();
|
||||
for (const binding of existing) {
|
||||
const key = bindingMatchKey(binding.match);
|
||||
@@ -320,10 +349,7 @@ export function applyAgentBindings(
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
routing: {
|
||||
...cfg.routing,
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
added,
|
||||
skipped,
|
||||
@@ -340,39 +366,41 @@ export function pruneAgentConfig(
|
||||
removedAllow: number;
|
||||
} {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const agents = { ...cfg.routing?.agents };
|
||||
delete agents[id];
|
||||
const nextAgents = Object.keys(agents).length > 0 ? agents : undefined;
|
||||
const agents = listAgentEntries(cfg);
|
||||
const nextAgentsList = agents.filter(
|
||||
(entry) => normalizeAgentId(entry.id) !== id,
|
||||
);
|
||||
const nextAgents = nextAgentsList.length > 0 ? nextAgentsList : undefined;
|
||||
|
||||
const bindings = cfg.routing?.bindings ?? [];
|
||||
const bindings = cfg.bindings ?? [];
|
||||
const filteredBindings = bindings.filter(
|
||||
(binding) => normalizeAgentId(binding.agentId) !== id,
|
||||
);
|
||||
|
||||
const allow = cfg.routing?.agentToAgent?.allow ?? [];
|
||||
const allow = cfg.tools?.agentToAgent?.allow ?? [];
|
||||
const filteredAllow = allow.filter((entry) => entry !== id);
|
||||
|
||||
const nextRouting = {
|
||||
...cfg.routing,
|
||||
...(nextAgents ? { agents: nextAgents } : {}),
|
||||
...(nextAgents ? {} : { agents: undefined }),
|
||||
bindings: filteredBindings.length > 0 ? filteredBindings : undefined,
|
||||
agentToAgent: cfg.routing?.agentToAgent
|
||||
? {
|
||||
...cfg.routing.agentToAgent,
|
||||
const nextAgentsConfig = cfg.agents
|
||||
? { ...cfg.agents, list: nextAgents }
|
||||
: nextAgents
|
||||
? { list: nextAgents }
|
||||
: undefined;
|
||||
const nextTools = cfg.tools?.agentToAgent
|
||||
? {
|
||||
...cfg.tools,
|
||||
agentToAgent: {
|
||||
...cfg.tools.agentToAgent,
|
||||
allow: filteredAllow.length > 0 ? filteredAllow : undefined,
|
||||
}
|
||||
: undefined,
|
||||
defaultAgentId:
|
||||
normalizeAgentId(cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID) === id
|
||||
? DEFAULT_AGENT_ID
|
||||
: cfg.routing?.defaultAgentId,
|
||||
};
|
||||
},
|
||||
}
|
||||
: cfg.tools;
|
||||
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
routing: nextRouting,
|
||||
agents: nextAgentsConfig,
|
||||
bindings: filteredBindings.length > 0 ? filteredBindings : undefined,
|
||||
tools: nextTools,
|
||||
},
|
||||
removedBindings: bindings.length - filteredBindings.length,
|
||||
removedAllow: allow.length - filteredAllow.length,
|
||||
@@ -632,7 +660,7 @@ export async function agentsListCommand(
|
||||
|
||||
const summaries = buildAgentSummaries(cfg);
|
||||
const bindingMap = new Map<string, AgentBinding[]>();
|
||||
for (const binding of cfg.routing?.bindings ?? []) {
|
||||
for (const binding of cfg.bindings ?? []) {
|
||||
const agentId = normalizeAgentId(binding.agentId);
|
||||
const list = bindingMap.get(agentId) ?? [];
|
||||
list.push(binding as AgentBinding);
|
||||
@@ -818,7 +846,7 @@ export async function agentsAddCommand(
|
||||
if (agentId !== nameInput) {
|
||||
runtime.log(`Normalized agent id to "${agentId}".`);
|
||||
}
|
||||
if (cfg.routing?.agents?.[agentId]) {
|
||||
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {
|
||||
runtime.error(`Agent "${agentId}" already exists.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
@@ -856,7 +884,9 @@ export async function agentsAddCommand(
|
||||
if (!opts.json) runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
|
||||
await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, {
|
||||
skipBootstrap: Boolean(bindingResult.config.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(
|
||||
bindingResult.config.agents?.defaults?.skipBootstrap,
|
||||
),
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -920,7 +950,9 @@ export async function agentsAddCommand(
|
||||
await prompter.note(`Normalized id to "${agentId}".`, "Agent id");
|
||||
}
|
||||
|
||||
const existingAgent = cfg.routing?.agents?.[agentId];
|
||||
const existingAgent = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === agentId,
|
||||
);
|
||||
if (existingAgent) {
|
||||
const shouldUpdate = await prompter.confirm({
|
||||
message: `Agent "${agentId}" already exists. Update it?`,
|
||||
@@ -1005,8 +1037,7 @@ export async function agentsAddCommand(
|
||||
|
||||
if (selection.length > 0) {
|
||||
const wantsBindings = await prompter.confirm({
|
||||
message:
|
||||
"Route selected providers to this agent now? (routing.bindings)",
|
||||
message: "Route selected providers to this agent now? (bindings)",
|
||||
initialValue: false,
|
||||
});
|
||||
if (wantsBindings) {
|
||||
@@ -1033,7 +1064,7 @@ export async function agentsAddCommand(
|
||||
} else {
|
||||
await prompter.note(
|
||||
[
|
||||
"Routing unchanged. Add routing.bindings when you're ready.",
|
||||
"Routing unchanged. Add bindings when you're ready.",
|
||||
"Docs: https://docs.clawd.bot/concepts/multi-agent",
|
||||
].join("\n"),
|
||||
"Routing",
|
||||
@@ -1044,7 +1075,7 @@ export async function agentsAddCommand(
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -1091,7 +1122,7 @@ export async function agentsDeleteCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cfg.routing?.agents?.[agentId]) {
|
||||
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
|
||||
runtime.error(`Agent "${agentId}" not found.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
|
||||
+36
-27
@@ -65,13 +65,16 @@ export async function warnIfModelConfigLooksOff(
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
? {
|
||||
...config,
|
||||
agent: {
|
||||
...config.agent,
|
||||
model: {
|
||||
...(typeof config.agent?.model === "object"
|
||||
? config.agent.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
agents: {
|
||||
...config.agents,
|
||||
defaults: {
|
||||
...config.agents?.defaults,
|
||||
model: {
|
||||
...(typeof config.agents?.defaults?.model === "object"
|
||||
? config.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -92,7 +95,7 @@ export async function warnIfModelConfigLooksOff(
|
||||
);
|
||||
if (!known) {
|
||||
warnings.push(
|
||||
`Model not found: ${ref.provider}/${ref.model}. Update agent.model or run /models list.`,
|
||||
`Model not found: ${ref.provider}/${ref.model}. Update agents.defaults.model or run /models list.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +114,7 @@ export async function warnIfModelConfigLooksOff(
|
||||
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
|
||||
if (hasCodex) {
|
||||
warnings.push(
|
||||
`Detected OpenAI Codex OAuth. Consider setting agent.model to ${OPENAI_CODEX_DEFAULT_MODEL}.`,
|
||||
`Detected OpenAI Codex OAuth. Consider setting agents.defaults.model to ${OPENAI_CODEX_DEFAULT_MODEL}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -454,30 +457,36 @@ export async function applyAuthChoice(params: {
|
||||
const modelKey = "google-antigravity/claude-opus-4-5-thinking";
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
models: {
|
||||
...nextConfig.agent?.models,
|
||||
[modelKey]: nextConfig.agent?.models?.[modelKey] ?? {},
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
models: {
|
||||
...nextConfig.agents?.defaults?.models,
|
||||
[modelKey]:
|
||||
nextConfig.agents?.defaults?.models?.[modelKey] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (params.setDefaultModel) {
|
||||
const existingModel = nextConfig.agents?.defaults?.model;
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
model: {
|
||||
...(nextConfig.agent?.model &&
|
||||
"fallbacks" in
|
||||
(nextConfig.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (
|
||||
nextConfig.agent.model as { fallbacks?: string[] }
|
||||
).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: modelKey,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: modelKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+57
-42
@@ -625,26 +625,32 @@ async function promptAuthConfig(
|
||||
mode: "oauth",
|
||||
});
|
||||
// Set default model to Claude Opus 4.5 via Antigravity
|
||||
const existingDefaults = next.agents?.defaults;
|
||||
const existingModel = existingDefaults?.model;
|
||||
const existingModels = existingDefaults?.models;
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "google-antigravity/claude-opus-4-5-thinking",
|
||||
},
|
||||
models: {
|
||||
...next.agent?.models,
|
||||
"google-antigravity/claude-opus-4-5-thinking":
|
||||
next.agent?.models?.[
|
||||
"google-antigravity/claude-opus-4-5-thinking"
|
||||
] ?? {},
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...existingDefaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "google-antigravity/claude-opus-4-5-thinking",
|
||||
},
|
||||
models: {
|
||||
...existingModels,
|
||||
"google-antigravity/claude-opus-4-5-thinking":
|
||||
existingModels?.[
|
||||
"google-antigravity/claude-opus-4-5-thinking"
|
||||
] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -714,9 +720,9 @@ async function promptAuthConfig(
|
||||
}
|
||||
|
||||
const currentModel =
|
||||
typeof next.agent?.model === "string"
|
||||
? next.agent?.model
|
||||
: (next.agent?.model?.primary ?? "");
|
||||
typeof next.agents?.defaults?.model === "string"
|
||||
? next.agents?.defaults?.model
|
||||
: (next.agents?.defaults?.model?.primary ?? "");
|
||||
const preferAnthropic =
|
||||
authChoice === "claude-cli" ||
|
||||
authChoice === "token" ||
|
||||
@@ -736,23 +742,29 @@ async function promptAuthConfig(
|
||||
);
|
||||
const model = String(modelInput ?? "").trim();
|
||||
if (model) {
|
||||
const existingDefaults = next.agents?.defaults;
|
||||
const existingModel = existingDefaults?.model;
|
||||
const existingModels = existingDefaults?.models;
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
models: {
|
||||
...next.agent?.models,
|
||||
[model]: next.agent?.models?.[model] ?? {},
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...existingDefaults,
|
||||
model: {
|
||||
...(existingModel &&
|
||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
models: {
|
||||
...existingModels,
|
||||
[model]: existingModels?.[model] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -955,7 +967,7 @@ export async function runConfigureWizard(
|
||||
{
|
||||
value: "workspace",
|
||||
label: "Workspace",
|
||||
hint: "Set agent workspace + ensure sessions",
|
||||
hint: "Set default workspace + ensure sessions",
|
||||
},
|
||||
{
|
||||
value: "model",
|
||||
@@ -999,8 +1011,8 @@ export async function runConfigureWizard(
|
||||
|
||||
let nextConfig = { ...baseConfig };
|
||||
let workspaceDir =
|
||||
nextConfig.agent?.workspace ??
|
||||
baseConfig.agent?.workspace ??
|
||||
nextConfig.agents?.defaults?.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
let gatewayToken: string | undefined;
|
||||
@@ -1018,9 +1030,12 @@ export async function runConfigureWizard(
|
||||
);
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agent: {
|
||||
...nextConfig.agent,
|
||||
workspace: workspaceDir,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
||||
|
||||
@@ -71,75 +71,184 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||
const changes: string[] = [];
|
||||
let next: ClawdbotConfig = cfg;
|
||||
|
||||
const workspace = cfg.agent?.workspace;
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== workspace) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
workspace: updatedWorkspace,
|
||||
},
|
||||
};
|
||||
changes.push(`Updated agent.workspace → ${updatedWorkspace}`);
|
||||
}
|
||||
const defaults = cfg.agents?.defaults;
|
||||
if (defaults) {
|
||||
let updatedDefaults = defaults;
|
||||
let defaultsChanged = false;
|
||||
|
||||
const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot;
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(workspaceRoot);
|
||||
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(defaults.workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== defaults.workspace) {
|
||||
updatedDefaults = { ...updatedDefaults, workspace: updatedWorkspace };
|
||||
defaultsChanged = true;
|
||||
changes.push(`Updated agents.defaults.workspace → ${updatedWorkspace}`);
|
||||
}
|
||||
|
||||
const sandbox = defaults.sandbox;
|
||||
if (sandbox) {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
`Updated agent.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dockerImage = cfg.agent?.sandbox?.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const dockerImage = sandbox.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...next.agent?.sandbox?.docker,
|
||||
...updatedSandbox.docker,
|
||||
image: updatedDockerImage,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(`Updated agent.sandbox.docker.image → ${updatedDockerImage}`);
|
||||
}
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.docker.image → ${updatedDockerImage}`,
|
||||
);
|
||||
}
|
||||
|
||||
const containerPrefix = cfg.agent?.sandbox?.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (updatedContainerPrefix && updatedContainerPrefix !== containerPrefix) {
|
||||
next = {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
sandbox: {
|
||||
...next.agent?.sandbox,
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...next.agent?.sandbox?.docker,
|
||||
...updatedSandbox.docker,
|
||||
containerPrefix: updatedContainerPrefix,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (sandboxChanged) {
|
||||
updatedDefaults = { ...updatedDefaults, sandbox: updatedSandbox };
|
||||
defaultsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultsChanged) {
|
||||
next = {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: updatedDefaults,
|
||||
},
|
||||
},
|
||||
};
|
||||
changes.push(
|
||||
`Updated agent.sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const list = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
||||
if (list.length > 0) {
|
||||
let listChanged = false;
|
||||
const nextList = list.map((agent) => {
|
||||
let updatedAgent = agent;
|
||||
let agentChanged = false;
|
||||
|
||||
const updatedWorkspace = normalizeDefaultWorkspacePath(agent.workspace);
|
||||
if (updatedWorkspace && updatedWorkspace !== agent.workspace) {
|
||||
updatedAgent = { ...updatedAgent, workspace: updatedWorkspace };
|
||||
agentChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") workspace → ${updatedWorkspace}`,
|
||||
);
|
||||
}
|
||||
|
||||
const sandbox = agent.sandbox;
|
||||
if (sandbox) {
|
||||
let updatedSandbox = sandbox;
|
||||
let sandboxChanged = false;
|
||||
|
||||
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(
|
||||
sandbox.workspaceRoot,
|
||||
);
|
||||
if (
|
||||
updatedWorkspaceRoot &&
|
||||
updatedWorkspaceRoot !== sandbox.workspaceRoot
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
workspaceRoot: updatedWorkspaceRoot,
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.workspaceRoot → ${updatedWorkspaceRoot}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dockerImage = sandbox.docker?.image;
|
||||
const updatedDockerImage = replaceLegacyName(dockerImage);
|
||||
if (updatedDockerImage && updatedDockerImage !== dockerImage) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...updatedSandbox.docker,
|
||||
image: updatedDockerImage,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.docker.image → ${updatedDockerImage}`,
|
||||
);
|
||||
}
|
||||
|
||||
const containerPrefix = sandbox.docker?.containerPrefix;
|
||||
const updatedContainerPrefix = replaceLegacyName(containerPrefix);
|
||||
if (
|
||||
updatedContainerPrefix &&
|
||||
updatedContainerPrefix !== containerPrefix
|
||||
) {
|
||||
updatedSandbox = {
|
||||
...updatedSandbox,
|
||||
docker: {
|
||||
...updatedSandbox.docker,
|
||||
containerPrefix: updatedContainerPrefix,
|
||||
},
|
||||
};
|
||||
sandboxChanged = true;
|
||||
changes.push(
|
||||
`Updated agents.list (id "${agent.id}") sandbox.docker.containerPrefix → ${updatedContainerPrefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (sandboxChanged) {
|
||||
updatedAgent = { ...updatedAgent, sandbox: updatedSandbox };
|
||||
agentChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (agentChanged) listChanged = true;
|
||||
return agentChanged ? updatedAgent : agent;
|
||||
});
|
||||
|
||||
if (listChanged) {
|
||||
next = {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
list: nextList,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { config: next, changes };
|
||||
@@ -170,18 +279,40 @@ export async function maybeMigrateLegacyConfigFile(runtime: RuntimeEnv) {
|
||||
typeof (legacySnapshot.parsed as ClawdbotConfig)?.gateway?.bind === "string"
|
||||
? (legacySnapshot.parsed as ClawdbotConfig).gateway?.bind
|
||||
: undefined;
|
||||
const agentWorkspace =
|
||||
typeof (legacySnapshot.parsed as ClawdbotConfig)?.agent?.workspace ===
|
||||
"string"
|
||||
? (legacySnapshot.parsed as ClawdbotConfig).agent?.workspace
|
||||
const parsed = legacySnapshot.parsed as Record<string, unknown>;
|
||||
const parsedAgents =
|
||||
parsed.agents && typeof parsed.agents === "object"
|
||||
? (parsed.agents as Record<string, unknown>)
|
||||
: undefined;
|
||||
const parsedDefaults =
|
||||
parsedAgents?.defaults && typeof parsedAgents.defaults === "object"
|
||||
? (parsedAgents.defaults as Record<string, unknown>)
|
||||
: undefined;
|
||||
const parsedLegacyAgent =
|
||||
parsed.agent && typeof parsed.agent === "object"
|
||||
? (parsed.agent as Record<string, unknown>)
|
||||
: undefined;
|
||||
const defaultWorkspace =
|
||||
typeof parsedDefaults?.workspace === "string"
|
||||
? parsedDefaults.workspace
|
||||
: undefined;
|
||||
const legacyWorkspace =
|
||||
typeof parsedLegacyAgent?.workspace === "string"
|
||||
? parsedLegacyAgent.workspace
|
||||
: undefined;
|
||||
const agentWorkspace = defaultWorkspace ?? legacyWorkspace;
|
||||
const workspaceLabel = defaultWorkspace
|
||||
? "agents.defaults.workspace"
|
||||
: legacyWorkspace
|
||||
? "agent.workspace"
|
||||
: "agents.defaults.workspace";
|
||||
|
||||
note(
|
||||
[
|
||||
`- File exists at ${legacyConfigPath}`,
|
||||
gatewayMode ? `- gateway.mode: ${gatewayMode}` : undefined,
|
||||
gatewayBind ? `- gateway.bind: ${gatewayBind}` : undefined,
|
||||
agentWorkspace ? `- agent.workspace: ${agentWorkspace}` : undefined,
|
||||
agentWorkspace ? `- ${workspaceLabel}: ${agentWorkspace}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
|
||||
@@ -96,12 +96,12 @@ async function dockerImageExists(image: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
function resolveSandboxDockerImage(cfg: ClawdbotConfig): string {
|
||||
const image = cfg.agent?.sandbox?.docker?.image?.trim();
|
||||
const image = cfg.agents?.defaults?.sandbox?.docker?.image?.trim();
|
||||
return image ? image : DEFAULT_SANDBOX_IMAGE;
|
||||
}
|
||||
|
||||
function resolveSandboxBrowserImage(cfg: ClawdbotConfig): string {
|
||||
const image = cfg.agent?.sandbox?.browser?.image?.trim();
|
||||
const image = cfg.agents?.defaults?.sandbox?.browser?.image?.trim();
|
||||
return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE;
|
||||
}
|
||||
|
||||
@@ -111,13 +111,16 @@ function updateSandboxDockerImage(
|
||||
): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
sandbox: {
|
||||
...cfg.agent?.sandbox,
|
||||
docker: {
|
||||
...cfg.agent?.sandbox?.docker,
|
||||
image,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
sandbox: {
|
||||
...cfg.agents?.defaults?.sandbox,
|
||||
docker: {
|
||||
...cfg.agents?.defaults?.sandbox?.docker,
|
||||
image,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -130,13 +133,16 @@ function updateSandboxBrowserImage(
|
||||
): ClawdbotConfig {
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
sandbox: {
|
||||
...cfg.agent?.sandbox,
|
||||
browser: {
|
||||
...cfg.agent?.sandbox?.browser,
|
||||
image,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
sandbox: {
|
||||
...cfg.agents?.defaults?.sandbox,
|
||||
browser: {
|
||||
...cfg.agents?.defaults?.sandbox?.browser,
|
||||
image,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -198,7 +204,7 @@ export async function maybeRepairSandboxImages(
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
): Promise<ClawdbotConfig> {
|
||||
const sandbox = cfg.agent?.sandbox;
|
||||
const sandbox = cfg.agents?.defaults?.sandbox;
|
||||
const mode = sandbox?.mode ?? "off";
|
||||
if (!sandbox || mode === "off") return cfg;
|
||||
|
||||
@@ -224,7 +230,7 @@ export async function maybeRepairSandboxImages(
|
||||
: undefined,
|
||||
updateConfig: (image) => {
|
||||
next = updateSandboxDockerImage(next, image);
|
||||
changes.push(`Updated agent.sandbox.docker.image → ${image}`);
|
||||
changes.push(`Updated agents.defaults.sandbox.docker.image → ${image}`);
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
@@ -239,7 +245,9 @@ export async function maybeRepairSandboxImages(
|
||||
buildScript: "scripts/sandbox-browser-setup.sh",
|
||||
updateConfig: (image) => {
|
||||
next = updateSandboxBrowserImage(next, image);
|
||||
changes.push(`Updated agent.sandbox.browser.image → ${image}`);
|
||||
changes.push(
|
||||
`Updated agents.defaults.sandbox.browser.image → ${image}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
@@ -255,11 +263,12 @@ export async function maybeRepairSandboxImages(
|
||||
}
|
||||
|
||||
export function noteSandboxScopeWarnings(cfg: ClawdbotConfig) {
|
||||
const globalSandbox = cfg.agent?.sandbox;
|
||||
const agents = cfg.routing?.agents ?? {};
|
||||
const globalSandbox = cfg.agents?.defaults?.sandbox;
|
||||
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const [agentId, agent] of Object.entries(agents)) {
|
||||
for (const agent of agents) {
|
||||
const agentId = agent.id;
|
||||
const agentSandbox = agent.sandbox;
|
||||
if (!agentSandbox) continue;
|
||||
|
||||
@@ -284,7 +293,7 @@ export function noteSandboxScopeWarnings(cfg: ClawdbotConfig) {
|
||||
if (overrides.length === 0) continue;
|
||||
|
||||
warnings.push(
|
||||
`- routing.agents.${agentId}.sandbox: ${overrides.join(
|
||||
`- agents.list (id "${agentId}") sandbox ${overrides.join(
|
||||
"/",
|
||||
)} overrides ignored (scope resolves to "shared").`,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
|
||||
import { note as clackNote } from "@clack/prompts";
|
||||
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
|
||||
const note = (message: string, title?: string) =>
|
||||
@@ -136,9 +136,7 @@ export async function noteStateIntegrity(
|
||||
const stateDir = resolveStateDir(env, homedir);
|
||||
const defaultStateDir = path.join(homedir(), ".clawdbot");
|
||||
const oauthDir = resolveOAuthDir(env, stateDir);
|
||||
const agentId = normalizeAgentId(
|
||||
cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(
|
||||
agentId,
|
||||
env,
|
||||
|
||||
@@ -186,9 +186,11 @@ describe("doctor legacy state migrations", () => {
|
||||
expect(result.changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("routes legacy state to routing.defaultAgentId", async () => {
|
||||
it("routes legacy state to the default agent entry", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: ClawdbotConfig = { routing: { defaultAgentId: "alpha" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { list: [{ id: "alpha", default: true }] },
|
||||
};
|
||||
const legacySessionsDir = path.join(root, "sessions");
|
||||
fs.mkdirSync(legacySessionsDir, { recursive: true });
|
||||
writeJson5(path.join(legacySessionsDir, "sessions.json"), {
|
||||
|
||||
+60
-47
@@ -344,13 +344,15 @@ describe("doctor", () => {
|
||||
raw: "{}",
|
||||
parsed: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -358,13 +360,15 @@ describe("doctor", () => {
|
||||
valid: true,
|
||||
config: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox",
|
||||
containerPrefix: "clawdbot-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -411,13 +415,15 @@ describe("doctor", () => {
|
||||
migrateLegacyConfig.mockReturnValueOnce({
|
||||
config: {
|
||||
gateway: { mode: "local", bind: "loopback" },
|
||||
agent: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdis-sandbox",
|
||||
containerPrefix: "clawdis-sbx",
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/steipete/clawd",
|
||||
sandbox: {
|
||||
workspaceRoot: "/Users/steipete/clawd/sandboxes",
|
||||
docker: {
|
||||
image: "clawdis-sandbox",
|
||||
containerPrefix: "clawdis-sbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -438,11 +444,12 @@ describe("doctor", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const agent = written.agent as Record<string, unknown>;
|
||||
const sandbox = agent.sandbox as Record<string, unknown>;
|
||||
const agents = written.agents as Record<string, unknown>;
|
||||
const defaults = agents.defaults as Record<string, unknown>;
|
||||
const sandbox = defaults.sandbox as Record<string, unknown>;
|
||||
const docker = sandbox.docker as Record<string, unknown>;
|
||||
|
||||
expect(agent.workspace).toBe("/Users/steipete/clawd");
|
||||
expect(defaults.workspace).toBe("/Users/steipete/clawd");
|
||||
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawd/sandboxes");
|
||||
expect(docker.image).toBe("clawdbot-sandbox");
|
||||
expect(docker.containerPrefix).toBe("clawdbot-sbx");
|
||||
@@ -456,15 +463,16 @@ describe("doctor", () => {
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "shared",
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
agents: {
|
||||
work: {
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
@@ -474,7 +482,7 @@ describe("doctor", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
issues: [],
|
||||
@@ -497,7 +505,7 @@ describe("doctor", () => {
|
||||
([message, title]) =>
|
||||
title === "Sandbox" &&
|
||||
typeof message === "string" &&
|
||||
message.includes("routing.agents.work.sandbox") &&
|
||||
message.includes('agents.list (id "work") sandbox docker') &&
|
||||
message.includes('scope resolves to "shared"'),
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -511,7 +519,7 @@ describe("doctor", () => {
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: { workspace: "/Users/steipete/clawd" },
|
||||
agents: { defaults: { workspace: "/Users/steipete/clawd" } },
|
||||
},
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
@@ -556,22 +564,26 @@ describe("doctor", () => {
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
docker: {
|
||||
image: "clawdbot-sandbox-common:bookworm-slim",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -614,8 +626,9 @@ describe("doctor", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const agent = written.agent as Record<string, unknown>;
|
||||
const sandbox = agent.sandbox as Record<string, unknown>;
|
||||
const agents = written.agents as Record<string, unknown>;
|
||||
const defaults = agents.defaults as Record<string, unknown>;
|
||||
const sandbox = defaults.sandbox as Record<string, unknown>;
|
||||
const docker = sandbox.docker as Record<string, unknown>;
|
||||
|
||||
expect(docker.image).toBe("clawdis-sandbox-common:bookworm-slim");
|
||||
|
||||
+12
-10
@@ -4,6 +4,10 @@ import {
|
||||
note as clackNote,
|
||||
outro as clackOutro,
|
||||
} from "@clack/prompts";
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -25,7 +29,7 @@ import { collectProvidersStatusIssues } from "../infra/providers-status-issues.j
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
@@ -69,11 +73,7 @@ import {
|
||||
shouldSuggestMemorySystem,
|
||||
} from "./doctor-workspace.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
printWizardHeader,
|
||||
} from "./onboard-helpers.js";
|
||||
import { applyWizardMetadata, printWizardHeader } from "./onboard-helpers.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
|
||||
const intro = (message: string) =>
|
||||
@@ -224,8 +224,9 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceDir = resolveUserPath(
|
||||
cfg.agent?.workspace ?? DEFAULT_WORKSPACE,
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const legacyWorkspace = detectLegacyWorkspaceDirs({ workspaceDir });
|
||||
if (legacyWorkspace.legacyDirs.length > 0) {
|
||||
@@ -415,8 +416,9 @@ export async function doctorCommand(
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
|
||||
if (options.workspaceSuggestions !== false) {
|
||||
const workspaceDir = resolveUserPath(
|
||||
cfg.agent?.workspace ?? DEFAULT_WORKSPACE,
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
noteWorkspaceBackupTip(workspaceDir);
|
||||
if (await shouldSuggestMemorySystem(workspaceDir)) {
|
||||
|
||||
@@ -8,28 +8,28 @@ import {
|
||||
|
||||
describe("applyGoogleGeminiModelDefault", () => {
|
||||
it("sets gemini default when model is unset", () => {
|
||||
const cfg: ClawdbotConfig = { agent: {} };
|
||||
const cfg: ClawdbotConfig = { agents: { defaults: {} } };
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides existing model", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5" } },
|
||||
};
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("no-ops when already gemini default", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
agents: { defaults: { model: GOOGLE_GEMINI_DEFAULT_MODEL } },
|
||||
};
|
||||
const applied = applyGoogleGeminiModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function applyGoogleGeminiModelDefault(cfg: ClawdbotConfig): {
|
||||
next: ClawdbotConfig;
|
||||
changed: boolean;
|
||||
} {
|
||||
const current = resolvePrimaryModel(cfg.agent?.model)?.trim();
|
||||
const current = resolvePrimaryModel(cfg.agents?.defaults?.model)?.trim();
|
||||
if (current === GOOGLE_GEMINI_DEFAULT_MODEL) {
|
||||
return { next: cfg, changed: false };
|
||||
}
|
||||
@@ -25,12 +25,19 @@ export function applyGoogleGeminiModelDefault(cfg: ClawdbotConfig): {
|
||||
return {
|
||||
next: {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model:
|
||||
cfg.agent?.model && typeof cfg.agent.model === "object"
|
||||
? { ...cfg.agent.model, primary: GOOGLE_GEMINI_DEFAULT_MODEL }
|
||||
: { primary: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model:
|
||||
cfg.agents?.defaults?.model &&
|
||||
typeof cfg.agents.defaults.model === "object"
|
||||
? {
|
||||
...cfg.agents.defaults.model,
|
||||
primary: GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
}
|
||||
: { primary: GOOGLE_GEMINI_DEFAULT_MODEL },
|
||||
},
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
|
||||
@@ -57,7 +57,9 @@ function makeRuntime() {
|
||||
|
||||
describe("models list/status", () => {
|
||||
it("models status resolves z.ai alias to canonical zai", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const { modelsStatusCommand } = await import("./models/list.js");
|
||||
@@ -69,7 +71,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models status plain outputs canonical zai model", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const { modelsStatusCommand } = await import("./models/list.js");
|
||||
@@ -80,7 +84,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list outputs canonical zai key for configured z.ai model", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
@@ -106,7 +112,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list plain outputs canonical zai key", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
@@ -131,7 +139,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes z.ai alias", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -171,7 +181,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes Z.AI alias casing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -211,7 +223,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes z-ai alias", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
@@ -251,7 +265,9 @@ describe("models list/status", () => {
|
||||
});
|
||||
|
||||
it("models list marks auth as unavailable when ZAI key is missing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
|
||||
@@ -39,9 +39,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +54,7 @@ describe("models set + fallbacks", () => {
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: { agent: { model: { fallbacks: [] } } },
|
||||
config: { agents: { defaults: { model: { fallbacks: [] } } } },
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
@@ -67,9 +69,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { fallbacks: ["zai/glm-4.7"] },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { fallbacks: ["zai/glm-4.7"] },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,9 +99,11 @@ describe("models set + fallbacks", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(written.agent).toEqual({
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
expect(written.agents).toEqual({
|
||||
defaults: {
|
||||
model: { primary: "zai/glm-4.7" },
|
||||
models: { "zai/glm-4.7": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function modelsAliasesListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const models = cfg.agent?.models ?? {};
|
||||
const models = cfg.agents?.defaults?.models ?? {};
|
||||
const aliases = Object.entries(models).reduce<Record<string, string>>(
|
||||
(acc, [modelKey, entry]) => {
|
||||
const alias = entry?.alias?.trim();
|
||||
@@ -53,7 +53,7 @@ export async function modelsAliasesAddCommand(
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg: loadConfig() });
|
||||
const _updated = await updateConfig((cfg) => {
|
||||
const modelKey = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
const existing = entry?.alias?.trim();
|
||||
if (existing && existing === alias && key !== modelKey) {
|
||||
@@ -64,9 +64,12 @@ export async function modelsAliasesAddCommand(
|
||||
nextModels[modelKey] = { ...existing, alias };
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models: nextModels,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models: nextModels,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -81,7 +84,7 @@ export async function modelsAliasesRemoveCommand(
|
||||
) {
|
||||
const alias = normalizeAlias(aliasRaw);
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
let found = false;
|
||||
for (const [key, entry] of Object.entries(nextModels)) {
|
||||
if (entry?.alias?.trim() === alias) {
|
||||
@@ -95,17 +98,22 @@ export async function modelsAliasesRemoveCommand(
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models: nextModels,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models: nextModels,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
if (
|
||||
!updated.agent?.models ||
|
||||
Object.values(updated.agent.models).every((entry) => !entry?.alias?.trim())
|
||||
!updated.agents?.defaults?.models ||
|
||||
Object.values(updated.agents.defaults.models).every(
|
||||
(entry) => !entry?.alias?.trim(),
|
||||
)
|
||||
) {
|
||||
runtime.log("No aliases configured.");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function modelsFallbacksListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const fallbacks = cfg.agent?.model?.fallbacks ?? [];
|
||||
const fallbacks = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify({ fallbacks }, null, 2));
|
||||
@@ -44,13 +44,13 @@ export async function modelsFallbacksAddCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[targetKey]) nextModels[targetKey] = {};
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.model?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
const existingKeys = existing
|
||||
.map((entry) =>
|
||||
resolveModelRefFromString({
|
||||
@@ -64,28 +64,31 @@ export async function modelsFallbacksAddCommand(
|
||||
|
||||
if (existingKeys.includes(targetKey)) return cfg;
|
||||
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agent?.model?.fallbacks ?? []).join(", ")}`,
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ export async function modelsFallbacksRemoveCommand(
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.model?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.model?.fallbacks ?? [];
|
||||
const filtered = existing.filter((entry) => {
|
||||
const resolvedEntry = resolveModelRefFromString({
|
||||
raw: String(entry ?? ""),
|
||||
@@ -118,19 +121,22 @@ export async function modelsFallbacksRemoveCommand(
|
||||
throw new Error(`Fallback not found: ${targetKey}`);
|
||||
}
|
||||
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -138,24 +144,27 @@ export async function modelsFallbacksRemoveCommand(
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Fallbacks: ${(updated.agent?.model?.fallbacks ?? []).join(", ")}`,
|
||||
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
await updateConfig((cfg) => {
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function modelsImageFallbacksListCommand(
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const fallbacks = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const fallbacks = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify({ fallbacks }, null, 2));
|
||||
@@ -44,13 +44,13 @@ export async function modelsImageFallbacksAddCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const targetKey = modelKey(resolved.provider, resolved.model);
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[targetKey]) nextModels[targetKey] = {};
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
const existingKeys = existing
|
||||
.map((entry) =>
|
||||
resolveModelRefFromString({
|
||||
@@ -64,28 +64,31 @@ export async function modelsImageFallbacksAddCommand(
|
||||
|
||||
if (existingKeys.includes(targetKey)) return cfg;
|
||||
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [...existing, targetKey],
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Image fallbacks: ${(updated.agent?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const existing = cfg.agent?.imageModel?.fallbacks ?? [];
|
||||
const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? [];
|
||||
const filtered = existing.filter((entry) => {
|
||||
const resolvedEntry = resolveModelRefFromString({
|
||||
raw: String(entry ?? ""),
|
||||
@@ -118,19 +121,22 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
throw new Error(`Image fallback not found: ${targetKey}`);
|
||||
}
|
||||
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: filtered,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -138,24 +144,27 @@ export async function modelsImageFallbacksRemoveCommand(
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(
|
||||
`Image fallbacks: ${(updated.agent?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) {
|
||||
await updateConfig((cfg) => {
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
: undefined),
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,9 +63,11 @@ const mocks = vi.hoisted(() => {
|
||||
.mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-opus-4-5", fallbacks: [] },
|
||||
models: { "anthropic/claude-opus-4-5": { alias: "Opus" } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-5", fallbacks: [] },
|
||||
models: { "anthropic/claude-opus-4-5": { alias: "Opus" } },
|
||||
},
|
||||
},
|
||||
models: { providers: {} },
|
||||
env: { shellEnv: { enabled: true } },
|
||||
|
||||
@@ -290,10 +290,10 @@ const resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
|
||||
|
||||
addEntry(resolvedDefault, "default");
|
||||
|
||||
const modelConfig = cfg.agent?.model as
|
||||
const modelConfig = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const imageModelConfig = cfg.agent?.imageModel as
|
||||
const imageModelConfig = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const modelFallbacks =
|
||||
@@ -333,7 +333,7 @@ const resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
|
||||
addEntry(resolved.ref, `img-fallback#${idx + 1}`);
|
||||
});
|
||||
|
||||
for (const key of Object.keys(cfg.agent?.models ?? {})) {
|
||||
for (const key of Object.keys(cfg.agents?.defaults?.models ?? {})) {
|
||||
const parsed = parseModelRef(String(key ?? ""), DEFAULT_PROVIDER);
|
||||
if (!parsed) continue;
|
||||
addEntry(parsed, "configured");
|
||||
@@ -623,11 +623,11 @@ export async function modelsStatusCommand(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
const modelConfig = cfg.agent?.model as
|
||||
const modelConfig = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
const imageConfig = cfg.agent?.imageModel as
|
||||
const imageConfig = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
@@ -645,14 +645,14 @@ export async function modelsStatusCommand(
|
||||
: (imageConfig?.primary?.trim() ?? "");
|
||||
const imageFallbacks =
|
||||
typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
|
||||
const aliases = Object.entries(cfg.agent?.models ?? {}).reduce<
|
||||
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<
|
||||
Record<string, string>
|
||||
>((acc, [key, entry]) => {
|
||||
const alias = entry?.alias?.trim();
|
||||
if (alias) acc[alias] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
const allowed = Object.keys(cfg.agent?.models ?? {});
|
||||
const allowed = Object.keys(cfg.agents?.defaults?.models ?? {});
|
||||
|
||||
const agentDir = resolveClawdbotAgentDir();
|
||||
const store = ensureAuthProfileStore();
|
||||
|
||||
@@ -327,14 +327,14 @@ export async function modelsScanCommand(
|
||||
}
|
||||
|
||||
const _updated = await updateConfig((cfg) => {
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
for (const entry of selected) {
|
||||
if (!nextModels[entry]) nextModels[entry] = {};
|
||||
}
|
||||
for (const entry of selectedImages) {
|
||||
if (!nextModels[entry]) nextModels[entry] = {};
|
||||
}
|
||||
const existingImageModel = cfg.agent?.imageModel as
|
||||
const existingImageModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const nextImageModel =
|
||||
@@ -346,12 +346,12 @@ export async function modelsScanCommand(
|
||||
fallbacks: selectedImages,
|
||||
...(opts.setImage ? { primary: selectedImages[0] } : {}),
|
||||
}
|
||||
: cfg.agent?.imageModel;
|
||||
const existingModel = cfg.agent?.model as
|
||||
: cfg.agents?.defaults?.imageModel;
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
const agent = {
|
||||
...cfg.agent,
|
||||
const defaults = {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.primary
|
||||
? { primary: existingModel.primary }
|
||||
@@ -361,10 +361,13 @@ export async function modelsScanCommand(
|
||||
},
|
||||
...(nextImageModel ? { imageModel: nextImageModel } : {}),
|
||||
models: nextModels,
|
||||
} satisfies NonNullable<typeof cfg.agent>;
|
||||
} satisfies NonNullable<NonNullable<typeof cfg.agents>["defaults"]>;
|
||||
return {
|
||||
...cfg,
|
||||
agent,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -9,26 +9,31 @@ export async function modelsSetImageCommand(
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const key = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[key]) nextModels[key] = {};
|
||||
const existingModel = cfg.agent?.imageModel as
|
||||
const existingModel = cfg.agents?.defaults?.imageModel as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
imageModel: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
imageModel: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(`Image model: ${updated.agent?.imageModel?.primary ?? modelRaw}`);
|
||||
runtime.log(
|
||||
`Image model: ${updated.agents?.defaults?.imageModel?.primary ?? modelRaw}`,
|
||||
);
|
||||
}
|
||||
|
||||
+16
-11
@@ -6,26 +6,31 @@ export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) {
|
||||
const updated = await updateConfig((cfg) => {
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const key = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agent?.models };
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
if (!nextModels[key]) nextModels[key] = {};
|
||||
const existingModel = cfg.agent?.model as
|
||||
const existingModel = cfg.agents?.defaults?.model as
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel?.fallbacks
|
||||
? { fallbacks: existingModel.fallbacks }
|
||||
: undefined),
|
||||
primary: key,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
runtime.log(`Default model: ${updated.agent?.model?.primary ?? modelRaw}`);
|
||||
runtime.log(
|
||||
`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function resolveModelTarget(params: {
|
||||
|
||||
export function buildAllowlistSet(cfg: ClawdbotConfig): Set<string> {
|
||||
const allowed = new Set<string>();
|
||||
const models = cfg.agent?.models ?? {};
|
||||
const models = cfg.agents?.defaults?.models ?? {};
|
||||
for (const raw of Object.keys(models)) {
|
||||
const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER);
|
||||
if (!parsed) continue;
|
||||
|
||||
@@ -126,7 +126,7 @@ export function applyAuthProfileConfig(
|
||||
export function applyMinimaxProviderConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): ClawdbotConfig {
|
||||
const models = { ...cfg.agent?.models };
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models["anthropic/claude-opus-4-5"] = {
|
||||
...models["anthropic/claude-opus-4-5"],
|
||||
alias: models["anthropic/claude-opus-4-5"]?.alias ?? "Opus",
|
||||
@@ -158,9 +158,12 @@ export function applyMinimaxProviderConfig(
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
@@ -224,17 +227,21 @@ export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const next = applyMinimaxProviderConfig(cfg);
|
||||
return {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "lmstudio/minimax-m2.1-gs32",
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(next.agents?.defaults?.model &&
|
||||
"fallbacks" in (next.agents.defaults.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (
|
||||
next.agents.defaults.model as { fallbacks?: string[] }
|
||||
).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: "lmstudio/minimax-m2.1-gs32",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,13 +36,13 @@ export function guardCancel<T>(value: T, runtime: RuntimeEnv): T {
|
||||
|
||||
export function summarizeExistingConfig(config: ClawdbotConfig): string {
|
||||
const rows: string[] = [];
|
||||
if (config.agent?.workspace)
|
||||
rows.push(`workspace: ${config.agent.workspace}`);
|
||||
if (config.agent?.model) {
|
||||
const defaults = config.agents?.defaults;
|
||||
if (defaults?.workspace) rows.push(`workspace: ${defaults.workspace}`);
|
||||
if (defaults?.model) {
|
||||
const model =
|
||||
typeof config.agent.model === "string"
|
||||
? config.agent.model
|
||||
: config.agent.model.primary;
|
||||
typeof defaults.model === "string"
|
||||
? defaults.model
|
||||
: defaults.model.primary;
|
||||
if (model) rows.push(`model: ${model}`);
|
||||
}
|
||||
if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`);
|
||||
|
||||
@@ -96,14 +96,21 @@ export async function runNonInteractiveOnboarding(
|
||||
}
|
||||
|
||||
const workspaceDir = resolveUserPath(
|
||||
(opts.workspace ?? baseConfig.agent?.workspace ?? DEFAULT_WORKSPACE).trim(),
|
||||
(
|
||||
opts.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE
|
||||
).trim(),
|
||||
);
|
||||
|
||||
let nextConfig: ClawdbotConfig = {
|
||||
...baseConfig,
|
||||
agent: {
|
||||
...baseConfig.agent,
|
||||
workspace: workspaceDir,
|
||||
agents: {
|
||||
...baseConfig.agents,
|
||||
defaults: {
|
||||
...baseConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
...baseConfig.gateway,
|
||||
@@ -311,7 +318,7 @@ export async function runNonInteractiveOnboarding(
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
});
|
||||
|
||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
|
||||
@@ -8,25 +8,29 @@ import {
|
||||
|
||||
describe("applyOpenAICodexModelDefault", () => {
|
||||
it("sets openai-codex default when model is unset", () => {
|
||||
const cfg: ClawdbotConfig = { agent: {} };
|
||||
const cfg: ClawdbotConfig = { agents: { defaults: {} } };
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets openai-codex default when model is openai/*", () => {
|
||||
const cfg: ClawdbotConfig = { agent: { model: "openai/gpt-5.2" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: "openai/gpt-5.2" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(true);
|
||||
expect(applied.next.agent?.model).toEqual({
|
||||
expect(applied.next.agents?.defaults?.model).toEqual({
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not override openai-codex/*", () => {
|
||||
const cfg: ClawdbotConfig = { agent: { model: "openai-codex/gpt-5.2" } };
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: "openai-codex/gpt-5.2" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
expect(applied.next).toEqual(cfg);
|
||||
@@ -34,7 +38,7 @@ describe("applyOpenAICodexModelDefault", () => {
|
||||
|
||||
it("does not override non-openai models", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5" } },
|
||||
};
|
||||
const applied = applyOpenAICodexModelDefault(cfg);
|
||||
expect(applied.changed).toBe(false);
|
||||
|
||||
@@ -26,19 +26,26 @@ export function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
|
||||
next: ClawdbotConfig;
|
||||
changed: boolean;
|
||||
} {
|
||||
const current = resolvePrimaryModel(cfg.agent?.model);
|
||||
const current = resolvePrimaryModel(cfg.agents?.defaults?.model);
|
||||
if (!shouldSetOpenAICodexModel(current)) {
|
||||
return { next: cfg, changed: false };
|
||||
}
|
||||
return {
|
||||
next: {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
model:
|
||||
cfg.agent?.model && typeof cfg.agent.model === "object"
|
||||
? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
|
||||
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model:
|
||||
cfg.agents?.defaults?.model &&
|
||||
typeof cfg.agents.defaults.model === "object"
|
||||
? {
|
||||
...cfg.agents.defaults.model,
|
||||
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
}
|
||||
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
|
||||
},
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
|
||||
@@ -12,10 +12,12 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => ({
|
||||
agent: {
|
||||
model: { primary: "pi:opus" },
|
||||
models: { "pi:opus": {} },
|
||||
contextTokens: 32000,
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "pi:opus" },
|
||||
models: { "pi:opus": {} },
|
||||
contextTokens: 32000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -169,7 +169,7 @@ export async function sessionsCommand(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const configContextTokens =
|
||||
cfg.agent?.contextTokens ??
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(resolved.model) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
|
||||
+11
-8
@@ -48,25 +48,28 @@ export async function setupCommand(
|
||||
|
||||
const existingRaw = await readConfigFileRaw();
|
||||
const cfg = existingRaw.parsed;
|
||||
const agent = cfg.agent ?? {};
|
||||
const defaults = cfg.agents?.defaults ?? {};
|
||||
|
||||
const workspace =
|
||||
desiredWorkspace ?? agent.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
desiredWorkspace ?? defaults.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
|
||||
const next: ClawdbotConfig = {
|
||||
...cfg,
|
||||
agent: {
|
||||
...agent,
|
||||
workspace,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...defaults,
|
||||
workspace,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!existingRaw.exists || agent.workspace !== workspace) {
|
||||
if (!existingRaw.exists || defaults.workspace !== workspace) {
|
||||
await writeConfigFile(next);
|
||||
runtime.log(
|
||||
!existingRaw.exists
|
||||
? `Wrote ${CONFIG_PATH_CLAWDBOT}`
|
||||
: `Updated ${CONFIG_PATH_CLAWDBOT} (set agent.workspace)`,
|
||||
: `Updated ${CONFIG_PATH_CLAWDBOT} (set agents.defaults.workspace)`,
|
||||
);
|
||||
} else {
|
||||
runtime.log(`Config OK: ${CONFIG_PATH_CLAWDBOT}`);
|
||||
@@ -74,7 +77,7 @@ export async function setupCommand(
|
||||
|
||||
const ws = await ensureAgentWorkspace({
|
||||
dir: workspace,
|
||||
ensureBootstrapFiles: !next.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !next.agents?.defaults?.skipBootstrap,
|
||||
});
|
||||
runtime.log(`Workspace OK: ${ws.dir}`);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function getStatusSummary(): Promise<StatusSummary> {
|
||||
});
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
const configContextTokens =
|
||||
cfg.agent?.contextTokens ??
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(configModel) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
|
||||
+14
-10
@@ -31,18 +31,18 @@ function canonicalizeAgentDir(agentDir: string): string {
|
||||
function collectReferencedAgentIds(cfg: ClawdbotConfig): string[] {
|
||||
const ids = new Set<string>();
|
||||
|
||||
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
|
||||
const defaultAgentId =
|
||||
cfg.routing?.defaultAgentId?.trim() || DEFAULT_AGENT_ID;
|
||||
agents.find((agent) => agent?.default)?.id ??
|
||||
agents[0]?.id ??
|
||||
DEFAULT_AGENT_ID;
|
||||
ids.add(normalizeAgentId(defaultAgentId));
|
||||
|
||||
const agents = cfg.routing?.agents;
|
||||
if (agents && typeof agents === "object") {
|
||||
for (const id of Object.keys(agents)) {
|
||||
ids.add(normalizeAgentId(id));
|
||||
}
|
||||
for (const entry of agents) {
|
||||
if (entry?.id) ids.add(normalizeAgentId(entry.id));
|
||||
}
|
||||
|
||||
const bindings = cfg.routing?.bindings;
|
||||
const bindings = cfg.bindings;
|
||||
if (Array.isArray(bindings)) {
|
||||
for (const binding of bindings) {
|
||||
const id = binding?.agentId;
|
||||
@@ -61,8 +61,12 @@ function resolveEffectiveAgentDir(
|
||||
deps?: { env?: NodeJS.ProcessEnv; homedir?: () => string },
|
||||
): string {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = cfg.routing?.agents?.[id]?.agentDir?.trim();
|
||||
if (configured) return resolveUserPath(configured);
|
||||
const configured = Array.isArray(cfg.agents?.list)
|
||||
? cfg.agents?.list.find((agent) => normalizeAgentId(agent.id) === id)
|
||||
?.agentDir
|
||||
: undefined;
|
||||
const trimmed = configured?.trim();
|
||||
if (trimmed) return resolveUserPath(trimmed);
|
||||
const root = resolveStateDir(
|
||||
deps?.env ?? process.env,
|
||||
deps?.homedir ?? os.homedir,
|
||||
@@ -102,7 +106,7 @@ export function formatDuplicateAgentDirError(
|
||||
(d) => `- ${d.agentDir}: ${d.agentIds.map((id) => `"${id}"`).join(", ")}`,
|
||||
),
|
||||
"",
|
||||
"Fix: remove the shared routing.agents.*.agentDir override (or give each agent its own directory).",
|
||||
"Fix: remove the shared agents.list[].agentDir override (or give each agent its own directory).",
|
||||
"If you want to share credentials, copy auth-profiles.json instead of sharing the entire agentDir.",
|
||||
];
|
||||
return lines.join("\n");
|
||||
|
||||
+197
-58
@@ -80,7 +80,7 @@ describe("config identity defaults", () => {
|
||||
process.env.HOME = previousHome;
|
||||
});
|
||||
|
||||
it("derives mentionPatterns when identity is set", async () => {
|
||||
it("does not derive mentionPatterns when identity is set", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -88,9 +88,19 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
messages: {},
|
||||
routing: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -103,13 +113,11 @@ describe("config identity defaults", () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.responsePrefix).toBeUndefined();
|
||||
expect(cfg.routing?.groupChat?.mentionPatterns).toEqual([
|
||||
"\\b@?Samantha\\b",
|
||||
]);
|
||||
expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults ackReaction to identity emoji", async () => {
|
||||
it("defaults ackReactionScope without setting ackReaction", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -117,7 +125,18 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
messages: {},
|
||||
},
|
||||
null,
|
||||
@@ -130,12 +149,12 @@ describe("config identity defaults", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.ackReaction).toBe("🦥");
|
||||
expect(cfg.messages?.ackReaction).toBeUndefined();
|
||||
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults ackReaction to 👀 when identity is missing", async () => {
|
||||
it("keeps ackReaction unset when identity is missing", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -155,7 +174,7 @@ describe("config identity defaults", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.ackReaction).toBe("👀");
|
||||
expect(cfg.messages?.ackReaction).toBeUndefined();
|
||||
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
|
||||
});
|
||||
});
|
||||
@@ -168,17 +187,22 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: {
|
||||
name: "Samantha Sloth",
|
||||
theme: "space lobster",
|
||||
emoji: "🦞",
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Samantha Sloth",
|
||||
theme: "space lobster",
|
||||
emoji: "🦞",
|
||||
},
|
||||
groupChat: { mentionPatterns: ["@clawd"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
messages: {
|
||||
responsePrefix: "✅",
|
||||
},
|
||||
routing: {
|
||||
groupChat: { mentionPatterns: ["@clawd"] },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -191,7 +215,9 @@ describe("config identity defaults", () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.responsePrefix).toBe("✅");
|
||||
expect(cfg.routing?.groupChat?.mentionPatterns).toEqual(["@clawd"]);
|
||||
expect(cfg.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual([
|
||||
"@clawd",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,7 +235,6 @@ describe("config identity defaults", () => {
|
||||
// legacy field should be ignored (moved to providers)
|
||||
textChunkLimit: 9999,
|
||||
},
|
||||
routing: {},
|
||||
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
|
||||
telegram: { enabled: true, textChunkLimit: 3333 },
|
||||
discord: {
|
||||
@@ -251,9 +276,19 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
messages: { responsePrefix: "" },
|
||||
routing: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -277,9 +312,7 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
|
||||
messages: {},
|
||||
routing: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -292,10 +325,8 @@ describe("config identity defaults", () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.responsePrefix).toBeUndefined();
|
||||
expect(cfg.routing?.groupChat?.mentionPatterns).toEqual([
|
||||
"\\b@?Samantha\\b",
|
||||
]);
|
||||
expect(cfg.agent).toBeUndefined();
|
||||
expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined();
|
||||
expect(cfg.agents).toBeUndefined();
|
||||
expect(cfg.session).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -308,9 +339,19 @@ describe("config identity defaults", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
identity: { name: "Clawd", theme: "space lobster", emoji: "🦞" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Clawd",
|
||||
theme: "space lobster",
|
||||
emoji: "🦞",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
messages: {},
|
||||
routing: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -411,7 +452,7 @@ describe("config pruning defaults", () => {
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify({ agent: {} }, null, 2),
|
||||
JSON.stringify({ agents: { defaults: {} } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
@@ -419,7 +460,7 @@ describe("config pruning defaults", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.agent?.contextPruning?.mode).toBe("adaptive");
|
||||
expect(cfg.agents?.defaults?.contextPruning?.mode).toBe("adaptive");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,7 +470,11 @@ describe("config pruning defaults", () => {
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify({ agent: { contextPruning: { mode: "off" } } }, null, 2),
|
||||
JSON.stringify(
|
||||
{ agents: { defaults: { contextPruning: { mode: "off" } } } },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
@@ -437,7 +482,7 @@ describe("config pruning defaults", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.agent?.contextPruning?.mode).toBe("off");
|
||||
expect(cfg.agents?.defaults?.contextPruning?.mode).toBe("off");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -850,6 +895,97 @@ describe("legacy config detection", () => {
|
||||
expect(res.config?.routing?.groupChat?.requireMention).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates routing.groupChat.mentionPatterns to messages.groupChat.mentionPatterns", async () => {
|
||||
vi.resetModules();
|
||||
const { migrateLegacyConfig } = await import("./config.js");
|
||||
const res = migrateLegacyConfig({
|
||||
routing: { groupChat: { mentionPatterns: ["@clawd"] } },
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
);
|
||||
expect(res.config?.messages?.groupChat?.mentionPatterns).toEqual([
|
||||
"@clawd",
|
||||
]);
|
||||
expect(res.config?.routing?.groupChat?.mentionPatterns).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates routing agentToAgent/queue/transcribeAudio to tools/messages/audio", async () => {
|
||||
vi.resetModules();
|
||||
const { migrateLegacyConfig } = await import("./config.js");
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
agentToAgent: { enabled: true, allow: ["main"] },
|
||||
queue: { mode: "queue", cap: 3 },
|
||||
transcribeAudio: { command: ["echo", "hi"], timeoutSeconds: 2 },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.agentToAgent → tools.agentToAgent.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved routing.queue → messages.queue.");
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.transcribeAudio → audio.transcription.",
|
||||
);
|
||||
expect(res.config?.tools?.agentToAgent).toEqual({
|
||||
enabled: true,
|
||||
allow: ["main"],
|
||||
});
|
||||
expect(res.config?.messages?.queue).toEqual({
|
||||
mode: "queue",
|
||||
cap: 3,
|
||||
});
|
||||
expect(res.config?.audio?.transcription).toEqual({
|
||||
command: ["echo", "hi"],
|
||||
timeoutSeconds: 2,
|
||||
});
|
||||
expect(res.config?.routing).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates agent config into agents.defaults and tools", async () => {
|
||||
vi.resetModules();
|
||||
const { migrateLegacyConfig } = await import("./config.js");
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "openai/gpt-5.2",
|
||||
tools: { allow: ["sessions.list"], deny: ["danger"] },
|
||||
elevated: { enabled: true, allowFrom: { discord: ["user:1"] } },
|
||||
bash: { timeoutSec: 12 },
|
||||
sandbox: { tools: { allow: ["browser.open"] } },
|
||||
subagents: { tools: { deny: ["sandbox"] } },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain("Moved agent.tools.allow → tools.allow.");
|
||||
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
|
||||
expect(res.changes).toContain("Moved agent.elevated → tools.elevated.");
|
||||
expect(res.changes).toContain("Moved agent.bash → tools.bash.");
|
||||
expect(res.changes).toContain(
|
||||
"Moved agent.sandbox.tools → tools.sandbox.tools.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved agent.subagents.tools → tools.subagents.tools.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved agent → agents.defaults.");
|
||||
expect(res.config?.agents?.defaults?.model).toEqual({
|
||||
primary: "openai/gpt-5.2",
|
||||
fallbacks: [],
|
||||
});
|
||||
expect(res.config?.tools?.allow).toEqual(["sessions.list"]);
|
||||
expect(res.config?.tools?.deny).toEqual(["danger"]);
|
||||
expect(res.config?.tools?.elevated).toEqual({
|
||||
enabled: true,
|
||||
allowFrom: { discord: ["user:1"] },
|
||||
});
|
||||
expect(res.config?.tools?.bash).toEqual({ timeoutSec: 12 });
|
||||
expect(res.config?.tools?.sandbox?.tools).toEqual({
|
||||
allow: ["browser.open"],
|
||||
});
|
||||
expect(res.config?.tools?.subagents?.tools).toEqual({
|
||||
deny: ["sandbox"],
|
||||
});
|
||||
expect((res.config as { agent?: unknown }).agent).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects telegram.requireMention", async () => {
|
||||
vi.resetModules();
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
@@ -1064,7 +1200,7 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe("agent.model");
|
||||
expect(res.issues.some((i) => i.path === "agent.model")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1095,22 +1231,25 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config?.agent?.model?.primary).toBe("anthropic/claude-opus-4-5");
|
||||
expect(res.config?.agent?.model?.fallbacks).toEqual([
|
||||
expect(res.config?.agents?.defaults?.model?.primary).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual([
|
||||
"openai/gpt-4.1-mini",
|
||||
]);
|
||||
expect(res.config?.agent?.imageModel?.primary).toBe("openai/gpt-4.1-mini");
|
||||
expect(res.config?.agent?.imageModel?.fallbacks).toEqual([
|
||||
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe(
|
||||
"openai/gpt-4.1-mini",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
]);
|
||||
expect(
|
||||
res.config?.agent?.models?.["anthropic/claude-opus-4-5"],
|
||||
res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"],
|
||||
).toMatchObject({ alias: "Opus" });
|
||||
expect(res.config?.agent?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||
expect(res.config?.agent?.allowedModels).toBeUndefined();
|
||||
expect(res.config?.agent?.modelAliases).toBeUndefined();
|
||||
expect(res.config?.agent?.modelFallbacks).toBeUndefined();
|
||||
expect(res.config?.agent?.imageModelFallbacks).toBeUndefined();
|
||||
expect(
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"],
|
||||
).toBeTruthy();
|
||||
expect(res.config?.agent).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces legacy issues in snapshot", async () => {
|
||||
@@ -1135,21 +1274,21 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
|
||||
describe("multi-agent agentDir validation", () => {
|
||||
it("rejects shared routing.agents.*.agentDir", async () => {
|
||||
it("rejects shared agents.list agentDir", async () => {
|
||||
vi.resetModules();
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const shared = path.join(os.tmpdir(), "clawdbot-shared-agentdir");
|
||||
const res = validateConfigObject({
|
||||
routing: {
|
||||
agents: {
|
||||
a: { agentDir: shared },
|
||||
b: { agentDir: shared },
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "a", agentDir: shared },
|
||||
{ id: "b", agentDir: shared },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path === "routing.agents")).toBe(true);
|
||||
expect(res.issues.some((i) => i.path === "agents.list")).toBe(true);
|
||||
expect(res.issues[0]?.message).toContain("Duplicate agentDir");
|
||||
}
|
||||
});
|
||||
@@ -1162,13 +1301,13 @@ describe("multi-agent agentDir validation", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
routing: {
|
||||
agents: {
|
||||
a: { agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||
b: { agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||
},
|
||||
bindings: [{ agentId: "a", match: { provider: "telegram" } }],
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "a", agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||
{ id: "b", agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||
],
|
||||
},
|
||||
bindings: [{ agentId: "a", match: { provider: "telegram" } }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
+17
-57
@@ -24,56 +24,13 @@ export type SessionDefaultsOptions = {
|
||||
warnState?: WarnState;
|
||||
};
|
||||
|
||||
function escapeRegExp(text: string): string {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function applyIdentityDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const identity = cfg.identity;
|
||||
if (!identity) return cfg;
|
||||
|
||||
const name = identity.name?.trim();
|
||||
|
||||
const routing = cfg.routing ?? {};
|
||||
const groupChat = routing.groupChat ?? {};
|
||||
|
||||
let mutated = false;
|
||||
const next: ClawdbotConfig = { ...cfg };
|
||||
|
||||
if (name && !groupChat.mentionPatterns) {
|
||||
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
|
||||
const re = parts.length ? parts.join("\\s+") : escapeRegExp(name);
|
||||
const pattern = `\\b@?${re}\\b`;
|
||||
next.routing = {
|
||||
...(next.routing ?? routing),
|
||||
groupChat: { ...groupChat, mentionPatterns: [pattern] },
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
return mutated ? next : cfg;
|
||||
}
|
||||
|
||||
export function applyMessageDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const messages = cfg.messages;
|
||||
const hasAckReaction = messages?.ackReaction !== undefined;
|
||||
const hasAckScope = messages?.ackReactionScope !== undefined;
|
||||
if (hasAckReaction && hasAckScope) return cfg;
|
||||
if (hasAckScope) return cfg;
|
||||
|
||||
const fallbackEmoji = cfg.identity?.emoji?.trim() || "👀";
|
||||
const nextMessages = messages ? { ...messages } : {};
|
||||
let mutated = false;
|
||||
|
||||
if (!hasAckReaction) {
|
||||
nextMessages.ackReaction = fallbackEmoji;
|
||||
mutated = true;
|
||||
}
|
||||
if (!hasAckScope) {
|
||||
nextMessages.ackReactionScope = "group-mentions";
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (!mutated) return cfg;
|
||||
nextMessages.ackReactionScope = "group-mentions";
|
||||
return {
|
||||
...cfg,
|
||||
messages: nextMessages,
|
||||
@@ -119,7 +76,7 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig {
|
||||
}
|
||||
|
||||
export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const existingAgent = cfg.agent;
|
||||
const existingAgent = cfg.agents?.defaults;
|
||||
if (!existingAgent) return cfg;
|
||||
const existingModels = existingAgent.models ?? {};
|
||||
if (Object.keys(existingModels).length === 0) return cfg;
|
||||
@@ -141,9 +98,9 @@ export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...existingAgent,
|
||||
models: nextModels,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: { ...existingAgent, models: nextModels },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -164,18 +121,21 @@ export function applyLoggingDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
export function applyContextPruningDefaults(
|
||||
cfg: ClawdbotConfig,
|
||||
): ClawdbotConfig {
|
||||
const agent = cfg.agent;
|
||||
if (!agent) return cfg;
|
||||
const contextPruning = agent?.contextPruning;
|
||||
const defaults = cfg.agents?.defaults;
|
||||
if (!defaults) return cfg;
|
||||
const contextPruning = defaults?.contextPruning;
|
||||
if (contextPruning?.mode) return cfg;
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...agent,
|
||||
contextPruning: {
|
||||
...contextPruning,
|
||||
mode: "adaptive",
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...defaults,
|
||||
contextPruning: {
|
||||
...contextPruning,
|
||||
mode: "adaptive",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+1
-4
@@ -14,7 +14,6 @@ import {
|
||||
} from "./agent-dirs.js";
|
||||
import {
|
||||
applyContextPruningDefaults,
|
||||
applyIdentityDefaults,
|
||||
applyLoggingDefaults,
|
||||
applyMessageDefaults,
|
||||
applyModelDefaults,
|
||||
@@ -165,9 +164,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
applyContextPruningDefaults(
|
||||
applySessionDefaults(
|
||||
applyLoggingDefaults(
|
||||
applyMessageDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
applyMessageDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+453
-16
@@ -12,53 +12,179 @@ type LegacyConfigMigration = {
|
||||
apply: (raw: Record<string, unknown>, changes: string[]) => void;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
|
||||
const getRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
isRecord(value) ? value : null;
|
||||
|
||||
const ensureRecord = (
|
||||
root: Record<string, unknown>,
|
||||
key: string,
|
||||
): Record<string, unknown> => {
|
||||
const existing = root[key];
|
||||
if (isRecord(existing)) return existing;
|
||||
const next: Record<string, unknown> = {};
|
||||
root[key] = next;
|
||||
return next;
|
||||
};
|
||||
|
||||
const mergeMissing = (
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>,
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value === undefined) continue;
|
||||
const existing = target[key];
|
||||
if (existing === undefined) {
|
||||
target[key] = value;
|
||||
continue;
|
||||
}
|
||||
if (isRecord(existing) && isRecord(value)) {
|
||||
mergeMissing(existing, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getAgentsList = (agents: Record<string, unknown> | null) => {
|
||||
const list = agents?.list;
|
||||
return Array.isArray(list) ? list : [];
|
||||
};
|
||||
|
||||
const resolveDefaultAgentIdFromRaw = (raw: Record<string, unknown>) => {
|
||||
const agents = getRecord(raw.agents);
|
||||
const list = getAgentsList(agents);
|
||||
const defaultEntry = list.find(
|
||||
(entry): entry is { id: string } =>
|
||||
isRecord(entry) &&
|
||||
entry.default === true &&
|
||||
typeof entry.id === "string" &&
|
||||
entry.id.trim() !== "",
|
||||
);
|
||||
if (defaultEntry) return defaultEntry.id.trim();
|
||||
const routing = getRecord(raw.routing);
|
||||
const routingDefault =
|
||||
typeof routing?.defaultAgentId === "string"
|
||||
? routing.defaultAgentId.trim()
|
||||
: "";
|
||||
if (routingDefault) return routingDefault;
|
||||
const firstEntry = list.find(
|
||||
(entry): entry is { id: string } =>
|
||||
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "",
|
||||
);
|
||||
if (firstEntry) return firstEntry.id.trim();
|
||||
return "main";
|
||||
};
|
||||
|
||||
const ensureAgentEntry = (
|
||||
list: unknown[],
|
||||
id: string,
|
||||
): Record<string, unknown> => {
|
||||
const normalized = id.trim();
|
||||
const existing = list.find(
|
||||
(entry): entry is Record<string, unknown> =>
|
||||
isRecord(entry) &&
|
||||
typeof entry.id === "string" &&
|
||||
entry.id.trim() === normalized,
|
||||
);
|
||||
if (existing) return existing;
|
||||
const created: Record<string, unknown> = { id: normalized };
|
||||
list.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["routing", "allowFrom"],
|
||||
message:
|
||||
"routing.allowFrom was removed; use whatsapp.allowFrom instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "bindings"],
|
||||
message:
|
||||
"routing.bindings was moved; use top-level bindings instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "agents"],
|
||||
message:
|
||||
"routing.agents was moved; use agents.list instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "defaultAgentId"],
|
||||
message:
|
||||
"routing.defaultAgentId was moved; use agents.list[].default instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "agentToAgent"],
|
||||
message:
|
||||
"routing.agentToAgent was moved; use tools.agentToAgent instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "requireMention"],
|
||||
message:
|
||||
'routing.groupChat.requireMention was removed; use whatsapp/telegram/imessage groups defaults (e.g. whatsapp.groups."*".requireMention) instead (run `clawdbot doctor` to migrate).',
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "mentionPatterns"],
|
||||
message:
|
||||
"routing.groupChat.mentionPatterns was moved; use agents.list[].groupChat.mentionPatterns or messages.groupChat.mentionPatterns instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "queue"],
|
||||
message:
|
||||
"routing.queue was moved; use messages.queue instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "transcribeAudio"],
|
||||
message:
|
||||
"routing.transcribeAudio was moved; use audio.transcription instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["telegram", "requireMention"],
|
||||
message:
|
||||
'telegram.requireMention was removed; use telegram.groups."*".requireMention instead (run `clawdbot doctor` to migrate).',
|
||||
},
|
||||
{
|
||||
path: ["identity"],
|
||||
message:
|
||||
"identity was moved; use agents.list[].identity instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent"],
|
||||
message:
|
||||
"agent.* was moved; use agents.defaults (and tools.* for tool/elevated/bash settings) instead (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "model"],
|
||||
message:
|
||||
"agent.model string was replaced by agent.model.primary/fallbacks and agent.models (run `clawdbot doctor` to migrate).",
|
||||
"agent.model string was replaced by agents.defaults.model.primary/fallbacks and agents.defaults.models (run `clawdbot doctor` to migrate).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModel"],
|
||||
message:
|
||||
"agent.imageModel string was replaced by agent.imageModel.primary/fallbacks (run `clawdbot doctor` to migrate).",
|
||||
"agent.imageModel string was replaced by agents.defaults.imageModel.primary/fallbacks (run `clawdbot doctor` to migrate).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "allowedModels"],
|
||||
message:
|
||||
"agent.allowedModels was replaced by agent.models (run `clawdbot doctor` to migrate).",
|
||||
"agent.allowedModels was replaced by agents.defaults.models (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelAliases"],
|
||||
message:
|
||||
"agent.modelAliases was replaced by agent.models.*.alias (run `clawdbot doctor` to migrate).",
|
||||
"agent.modelAliases was replaced by agents.defaults.models.*.alias (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelFallbacks"],
|
||||
message:
|
||||
"agent.modelFallbacks was replaced by agent.model.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
"agent.modelFallbacks was replaced by agents.defaults.model.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModelFallbacks"],
|
||||
message:
|
||||
"agent.imageModelFallbacks was replaced by agent.imageModel.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
"agent.imageModelFallbacks was replaced by agents.defaults.imageModel.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["gateway", "token"],
|
||||
@@ -236,11 +362,11 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||
describe:
|
||||
"Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
||||
apply: (raw, changes) => {
|
||||
const agent =
|
||||
raw.agent && typeof raw.agent === "object"
|
||||
? (raw.agent as Record<string, unknown>)
|
||||
: null;
|
||||
const agentRoot = getRecord(raw.agent);
|
||||
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
||||
const agent = agentRoot ?? defaults;
|
||||
if (!agent) return;
|
||||
const label = agentRoot ? "agent" : "agents.defaults";
|
||||
|
||||
const legacyModel =
|
||||
typeof agent.model === "string" ? String(agent.model) : undefined;
|
||||
@@ -358,26 +484,32 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||
agent.models = models;
|
||||
|
||||
if (legacyModel !== undefined) {
|
||||
changes.push("Migrated agent.model string → agent.model.primary.");
|
||||
changes.push(
|
||||
`Migrated ${label}.model string → ${label}.model.primary.`,
|
||||
);
|
||||
}
|
||||
if (legacyModelFallbacks.length > 0) {
|
||||
changes.push("Migrated agent.modelFallbacks → agent.model.fallbacks.");
|
||||
changes.push(
|
||||
`Migrated ${label}.modelFallbacks → ${label}.model.fallbacks.`,
|
||||
);
|
||||
}
|
||||
if (legacyImageModel !== undefined) {
|
||||
changes.push(
|
||||
"Migrated agent.imageModel string → agent.imageModel.primary.",
|
||||
`Migrated ${label}.imageModel string → ${label}.imageModel.primary.`,
|
||||
);
|
||||
}
|
||||
if (legacyImageModelFallbacks.length > 0) {
|
||||
changes.push(
|
||||
"Migrated agent.imageModelFallbacks → agent.imageModel.fallbacks.",
|
||||
`Migrated ${label}.imageModelFallbacks → ${label}.imageModel.fallbacks.`,
|
||||
);
|
||||
}
|
||||
if (legacyAllowed.length > 0) {
|
||||
changes.push("Migrated agent.allowedModels → agent.models.");
|
||||
changes.push(`Migrated ${label}.allowedModels → ${label}.models.`);
|
||||
}
|
||||
if (Object.keys(legacyAliases).length > 0) {
|
||||
changes.push("Migrated agent.modelAliases → agent.models.*.alias.");
|
||||
changes.push(
|
||||
`Migrated ${label}.modelAliases → ${label}.models.*.alias.`,
|
||||
);
|
||||
}
|
||||
|
||||
delete agent.allowedModels;
|
||||
@@ -386,6 +518,311 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||
delete agent.imageModelFallbacks;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.agents-v2",
|
||||
describe: "Move routing.agents/defaultAgentId to agents.list",
|
||||
apply: (raw, changes) => {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing) return;
|
||||
|
||||
const routingAgents = getRecord(routing.agents);
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const list = getAgentsList(agents);
|
||||
|
||||
if (routingAgents) {
|
||||
for (const [rawId, entryRaw] of Object.entries(routingAgents)) {
|
||||
const agentId = String(rawId ?? "").trim();
|
||||
const entry = getRecord(entryRaw);
|
||||
if (!agentId || !entry) continue;
|
||||
|
||||
const target = ensureAgentEntry(list, agentId);
|
||||
const entryCopy: Record<string, unknown> = { ...entry };
|
||||
|
||||
if ("mentionPatterns" in entryCopy) {
|
||||
const mentionPatterns = entryCopy.mentionPatterns;
|
||||
const groupChat = ensureRecord(target, "groupChat");
|
||||
if (groupChat.mentionPatterns === undefined) {
|
||||
groupChat.mentionPatterns = mentionPatterns;
|
||||
changes.push(
|
||||
`Moved routing.agents.${agentId}.mentionPatterns → agents.list (id "${agentId}").groupChat.mentionPatterns.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
`Removed routing.agents.${agentId}.mentionPatterns (agents.list groupChat mentionPatterns already set).`,
|
||||
);
|
||||
}
|
||||
delete entryCopy.mentionPatterns;
|
||||
}
|
||||
|
||||
const legacyGroupChat = getRecord(entryCopy.groupChat);
|
||||
if (legacyGroupChat) {
|
||||
const groupChat = ensureRecord(target, "groupChat");
|
||||
mergeMissing(groupChat, legacyGroupChat);
|
||||
delete entryCopy.groupChat;
|
||||
}
|
||||
|
||||
const legacySandbox = getRecord(entryCopy.sandbox);
|
||||
if (legacySandbox) {
|
||||
const sandboxTools = getRecord(legacySandbox.tools);
|
||||
if (sandboxTools) {
|
||||
const tools = ensureRecord(target, "tools");
|
||||
const sandbox = ensureRecord(tools, "sandbox");
|
||||
const toolPolicy = ensureRecord(sandbox, "tools");
|
||||
mergeMissing(toolPolicy, sandboxTools);
|
||||
delete legacySandbox.tools;
|
||||
changes.push(
|
||||
`Moved routing.agents.${agentId}.sandbox.tools → agents.list (id "${agentId}").tools.sandbox.tools.`,
|
||||
);
|
||||
}
|
||||
entryCopy.sandbox = legacySandbox;
|
||||
}
|
||||
|
||||
mergeMissing(target, entryCopy);
|
||||
}
|
||||
delete routing.agents;
|
||||
changes.push("Moved routing.agents → agents.list.");
|
||||
}
|
||||
|
||||
const defaultAgentId =
|
||||
typeof routing.defaultAgentId === "string"
|
||||
? routing.defaultAgentId.trim()
|
||||
: "";
|
||||
if (defaultAgentId) {
|
||||
const hasDefault = list.some(
|
||||
(entry): entry is Record<string, unknown> =>
|
||||
isRecord(entry) && entry.default === true,
|
||||
);
|
||||
if (!hasDefault) {
|
||||
const entry = ensureAgentEntry(list, defaultAgentId);
|
||||
entry.default = true;
|
||||
changes.push(
|
||||
`Moved routing.defaultAgentId → agents.list (id "${defaultAgentId}").default.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.defaultAgentId (agents.list default already set).",
|
||||
);
|
||||
}
|
||||
delete routing.defaultAgentId;
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
agents.list = list;
|
||||
}
|
||||
|
||||
if (Object.keys(routing).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.config-v2",
|
||||
describe:
|
||||
"Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio",
|
||||
apply: (raw, changes) => {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing) return;
|
||||
|
||||
if (routing.bindings !== undefined) {
|
||||
if (raw.bindings === undefined) {
|
||||
raw.bindings = routing.bindings;
|
||||
changes.push("Moved routing.bindings → bindings.");
|
||||
} else {
|
||||
changes.push("Removed routing.bindings (bindings already set).");
|
||||
}
|
||||
delete routing.bindings;
|
||||
}
|
||||
|
||||
if (routing.agentToAgent !== undefined) {
|
||||
const tools = ensureRecord(raw, "tools");
|
||||
if (tools.agentToAgent === undefined) {
|
||||
tools.agentToAgent = routing.agentToAgent;
|
||||
changes.push("Moved routing.agentToAgent → tools.agentToAgent.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.agentToAgent (tools.agentToAgent already set).",
|
||||
);
|
||||
}
|
||||
delete routing.agentToAgent;
|
||||
}
|
||||
|
||||
if (routing.queue !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
if (messages.queue === undefined) {
|
||||
messages.queue = routing.queue;
|
||||
changes.push("Moved routing.queue → messages.queue.");
|
||||
} else {
|
||||
changes.push("Removed routing.queue (messages.queue already set).");
|
||||
}
|
||||
delete routing.queue;
|
||||
}
|
||||
|
||||
const groupChat = getRecord(routing.groupChat);
|
||||
if (groupChat) {
|
||||
const historyLimit = groupChat.historyLimit;
|
||||
if (historyLimit !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup.historyLimit === undefined) {
|
||||
messagesGroup.historyLimit = historyLimit;
|
||||
changes.push(
|
||||
"Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit.",
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.groupChat.historyLimit (messages.groupChat.historyLimit already set).",
|
||||
);
|
||||
}
|
||||
delete groupChat.historyLimit;
|
||||
}
|
||||
|
||||
const mentionPatterns = groupChat.mentionPatterns;
|
||||
if (mentionPatterns !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup.mentionPatterns === undefined) {
|
||||
messagesGroup.mentionPatterns = mentionPatterns;
|
||||
changes.push(
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.groupChat.mentionPatterns (messages.groupChat.mentionPatterns already set).",
|
||||
);
|
||||
}
|
||||
delete groupChat.mentionPatterns;
|
||||
}
|
||||
|
||||
if (Object.keys(groupChat).length === 0) {
|
||||
delete routing.groupChat;
|
||||
} else {
|
||||
routing.groupChat = groupChat;
|
||||
}
|
||||
}
|
||||
|
||||
if (routing.transcribeAudio !== undefined) {
|
||||
const audio = ensureRecord(raw, "audio");
|
||||
if (audio.transcription === undefined) {
|
||||
audio.transcription = routing.transcribeAudio;
|
||||
changes.push("Moved routing.transcribeAudio → audio.transcription.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.transcribeAudio (audio.transcription already set).",
|
||||
);
|
||||
}
|
||||
delete routing.transcribeAudio;
|
||||
}
|
||||
|
||||
if (Object.keys(routing).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agent.defaults-v2",
|
||||
describe: "Move agent config to agents.defaults and tools",
|
||||
apply: (raw, changes) => {
|
||||
const agent = getRecord(raw.agent);
|
||||
if (!agent) return;
|
||||
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const defaults = getRecord(agents.defaults) ?? {};
|
||||
const tools = ensureRecord(raw, "tools");
|
||||
|
||||
const agentTools = getRecord(agent.tools);
|
||||
if (agentTools) {
|
||||
if (tools.allow === undefined && agentTools.allow !== undefined) {
|
||||
tools.allow = agentTools.allow;
|
||||
changes.push("Moved agent.tools.allow → tools.allow.");
|
||||
}
|
||||
if (tools.deny === undefined && agentTools.deny !== undefined) {
|
||||
tools.deny = agentTools.deny;
|
||||
changes.push("Moved agent.tools.deny → tools.deny.");
|
||||
}
|
||||
}
|
||||
|
||||
const elevated = getRecord(agent.elevated);
|
||||
if (elevated) {
|
||||
if (tools.elevated === undefined) {
|
||||
tools.elevated = elevated;
|
||||
changes.push("Moved agent.elevated → tools.elevated.");
|
||||
} else {
|
||||
changes.push("Removed agent.elevated (tools.elevated already set).");
|
||||
}
|
||||
}
|
||||
|
||||
const bash = getRecord(agent.bash);
|
||||
if (bash) {
|
||||
if (tools.bash === undefined) {
|
||||
tools.bash = bash;
|
||||
changes.push("Moved agent.bash → tools.bash.");
|
||||
} else {
|
||||
changes.push("Removed agent.bash (tools.bash already set).");
|
||||
}
|
||||
}
|
||||
|
||||
const sandbox = getRecord(agent.sandbox);
|
||||
if (sandbox) {
|
||||
const sandboxTools = getRecord(sandbox.tools);
|
||||
if (sandboxTools) {
|
||||
const toolsSandbox = ensureRecord(tools, "sandbox");
|
||||
const toolPolicy = ensureRecord(toolsSandbox, "tools");
|
||||
mergeMissing(toolPolicy, sandboxTools);
|
||||
delete sandbox.tools;
|
||||
changes.push("Moved agent.sandbox.tools → tools.sandbox.tools.");
|
||||
}
|
||||
}
|
||||
|
||||
const subagents = getRecord(agent.subagents);
|
||||
if (subagents) {
|
||||
const subagentTools = getRecord(subagents.tools);
|
||||
if (subagentTools) {
|
||||
const toolsSubagents = ensureRecord(tools, "subagents");
|
||||
const toolPolicy = ensureRecord(toolsSubagents, "tools");
|
||||
mergeMissing(toolPolicy, subagentTools);
|
||||
delete subagents.tools;
|
||||
changes.push("Moved agent.subagents.tools → tools.subagents.tools.");
|
||||
}
|
||||
}
|
||||
|
||||
const agentCopy: Record<string, unknown> = structuredClone(agent);
|
||||
delete agentCopy.tools;
|
||||
delete agentCopy.elevated;
|
||||
delete agentCopy.bash;
|
||||
if (isRecord(agentCopy.sandbox)) delete agentCopy.sandbox.tools;
|
||||
if (isRecord(agentCopy.subagents)) delete agentCopy.subagents.tools;
|
||||
|
||||
mergeMissing(defaults, agentCopy);
|
||||
agents.defaults = defaults;
|
||||
raw.agents = agents;
|
||||
delete raw.agent;
|
||||
changes.push("Moved agent → agents.defaults.");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "identity->agents.list",
|
||||
describe: "Move identity to agents.list[].identity",
|
||||
apply: (raw, changes) => {
|
||||
const identity = getRecord(raw.identity);
|
||||
if (!identity) return;
|
||||
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const list = getAgentsList(agents);
|
||||
const defaultId = resolveDefaultAgentIdFromRaw(raw);
|
||||
const entry = ensureAgentEntry(list, defaultId);
|
||||
if (entry.identity === undefined) {
|
||||
entry.identity = identity;
|
||||
changes.push(
|
||||
`Moved identity → agents.list (id "${defaultId}").identity.`,
|
||||
);
|
||||
} else {
|
||||
changes.push("Removed identity (agents.list identity already set).");
|
||||
}
|
||||
agents.list = list;
|
||||
raw.agents = agents;
|
||||
delete raw.identity;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] {
|
||||
|
||||
@@ -5,52 +5,62 @@ import type { ClawdbotConfig } from "./types.js";
|
||||
describe("applyModelDefaults", () => {
|
||||
it("adds default aliases when models are present", () => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-5.2": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe(
|
||||
"opus",
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias,
|
||||
).toBe("opus");
|
||||
expect(next.agents?.defaults?.models?.["openai/gpt-5.2"]?.alias).toBe(
|
||||
"gpt",
|
||||
);
|
||||
expect(next.agent?.models?.["openai/gpt-5.2"]?.alias).toBe("gpt");
|
||||
});
|
||||
|
||||
it("does not override existing aliases", () => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe(
|
||||
"Opus",
|
||||
);
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias,
|
||||
).toBe("Opus");
|
||||
});
|
||||
|
||||
it("respects explicit empty alias disables", () => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
models: {
|
||||
"google/gemini-3-pro-preview": { alias: "" },
|
||||
"google/gemini-3-flash-preview": {},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"google/gemini-3-pro-preview": { alias: "" },
|
||||
"google/gemini-3-flash-preview": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.models?.["google/gemini-3-pro-preview"]?.alias).toBe("");
|
||||
expect(next.agent?.models?.["google/gemini-3-flash-preview"]?.alias).toBe(
|
||||
"gemini-flash",
|
||||
);
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["google/gemini-3-pro-preview"]?.alias,
|
||||
).toBe("");
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["google/gemini-3-flash-preview"]?.alias,
|
||||
).toBe("gemini-flash");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("config schema", () => {
|
||||
const res = buildConfigSchema();
|
||||
const schema = res.schema as { properties?: Record<string, unknown> };
|
||||
expect(schema.properties?.gateway).toBeTruthy();
|
||||
expect(schema.properties?.agent).toBeTruthy();
|
||||
expect(schema.properties?.agents).toBeTruthy();
|
||||
expect(res.uiHints.gateway?.label).toBe("Gateway");
|
||||
expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true);
|
||||
expect(res.version).toBeTruthy();
|
||||
|
||||
+38
-36
@@ -24,13 +24,14 @@ export type ConfigSchemaResponse = {
|
||||
};
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
identity: "Identity",
|
||||
wizard: "Wizard",
|
||||
logging: "Logging",
|
||||
gateway: "Gateway",
|
||||
agent: "Agent",
|
||||
agents: "Agents",
|
||||
tools: "Tools",
|
||||
bindings: "Bindings",
|
||||
audio: "Audio",
|
||||
models: "Models",
|
||||
routing: "Routing",
|
||||
messages: "Messages",
|
||||
commands: "Commands",
|
||||
session: "Session",
|
||||
@@ -52,30 +53,31 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const GROUP_ORDER: Record<string, number> = {
|
||||
identity: 10,
|
||||
wizard: 20,
|
||||
gateway: 30,
|
||||
agent: 40,
|
||||
models: 50,
|
||||
routing: 60,
|
||||
messages: 70,
|
||||
commands: 75,
|
||||
session: 80,
|
||||
cron: 90,
|
||||
hooks: 100,
|
||||
ui: 110,
|
||||
browser: 120,
|
||||
talk: 130,
|
||||
telegram: 140,
|
||||
discord: 150,
|
||||
slack: 155,
|
||||
signal: 160,
|
||||
imessage: 170,
|
||||
whatsapp: 180,
|
||||
skills: 190,
|
||||
discovery: 200,
|
||||
presence: 210,
|
||||
voicewake: 220,
|
||||
agents: 40,
|
||||
tools: 50,
|
||||
bindings: 55,
|
||||
audio: 60,
|
||||
models: 70,
|
||||
messages: 80,
|
||||
commands: 85,
|
||||
session: 90,
|
||||
cron: 100,
|
||||
hooks: 110,
|
||||
ui: 120,
|
||||
browser: 130,
|
||||
talk: 140,
|
||||
telegram: 150,
|
||||
discord: 160,
|
||||
slack: 165,
|
||||
signal: 170,
|
||||
imessage: 180,
|
||||
whatsapp: 190,
|
||||
skills: 200,
|
||||
discovery: 210,
|
||||
presence: 220,
|
||||
voicewake: 230,
|
||||
logging: 900,
|
||||
};
|
||||
|
||||
@@ -90,14 +92,14 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
"gateway.reload.mode": "Config Reload Mode",
|
||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||
"agent.workspace": "Workspace",
|
||||
"agents.defaults.workspace": "Workspace",
|
||||
"auth.profiles": "Auth Profiles",
|
||||
"auth.order": "Auth Profile Order",
|
||||
"agent.models": "Models",
|
||||
"agent.model.primary": "Primary Model",
|
||||
"agent.model.fallbacks": "Model Fallbacks",
|
||||
"agent.imageModel.primary": "Image Model",
|
||||
"agent.imageModel.fallbacks": "Image Model Fallbacks",
|
||||
"agents.defaults.models": "Models",
|
||||
"agents.defaults.model.primary": "Primary Model",
|
||||
"agents.defaults.model.fallbacks": "Model Fallbacks",
|
||||
"agents.defaults.imageModel.primary": "Image Model",
|
||||
"agents.defaults.imageModel.fallbacks": "Image Model Fallbacks",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.text": "Text Commands",
|
||||
"commands.restart": "Allow Restart",
|
||||
@@ -154,14 +156,14 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
||||
"auth.order":
|
||||
"Ordered auth profile IDs per provider (used for automatic failover).",
|
||||
"agent.models":
|
||||
"agents.defaults.models":
|
||||
"Configured model catalog (keys are full provider/model IDs).",
|
||||
"agent.model.primary": "Primary model (provider/model).",
|
||||
"agent.model.fallbacks":
|
||||
"agents.defaults.model.primary": "Primary model (provider/model).",
|
||||
"agents.defaults.model.fallbacks":
|
||||
"Ordered fallback models (provider/model). Used when the primary model fails.",
|
||||
"agent.imageModel.primary":
|
||||
"agents.defaults.imageModel.primary":
|
||||
"Optional image model (provider/model) used when the primary model lacks image input.",
|
||||
"agent.imageModel.fallbacks":
|
||||
"agents.defaults.imageModel.fallbacks":
|
||||
"Ordered fallback image models (provider/model).",
|
||||
"commands.native":
|
||||
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
|
||||
|
||||
@@ -217,12 +217,15 @@ export function resolveStorePath(store?: string, opts?: { agentId?: string }) {
|
||||
|
||||
export function resolveMainSessionKey(cfg?: {
|
||||
session?: { scope?: SessionScope; mainKey?: string };
|
||||
routing?: { defaultAgentId?: string };
|
||||
agents?: { list?: Array<{ id?: string; default?: boolean }> };
|
||||
}): string {
|
||||
if (cfg?.session?.scope === "global") return "global";
|
||||
const agentId = normalizeAgentId(
|
||||
cfg?.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
const agents = cfg?.agents?.list ?? [];
|
||||
const defaultAgentId =
|
||||
agents.find((agent) => agent?.default)?.id ??
|
||||
agents[0]?.id ??
|
||||
DEFAULT_AGENT_ID;
|
||||
const agentId = normalizeAgentId(defaultAgentId);
|
||||
const mainKey =
|
||||
(cfg?.session?.mainKey ?? DEFAULT_MAIN_KEY).trim() || DEFAULT_MAIN_KEY;
|
||||
return buildAgentMainSessionKey({ agentId, mainKey });
|
||||
|
||||
+230
-207
@@ -91,6 +91,12 @@ export type AgentElevatedAllowFromConfig = {
|
||||
webchat?: Array<string | number>;
|
||||
};
|
||||
|
||||
export type IdentityConfig = {
|
||||
name?: string;
|
||||
theme?: string;
|
||||
emoji?: string;
|
||||
};
|
||||
|
||||
export type WhatsAppActionConfig = {
|
||||
reactions?: boolean;
|
||||
sendMessage?: boolean;
|
||||
@@ -762,83 +768,133 @@ export type GroupChatConfig = {
|
||||
historyLimit?: number;
|
||||
};
|
||||
|
||||
export type RoutingConfig = {
|
||||
transcribeAudio?: {
|
||||
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
|
||||
command: string[];
|
||||
timeoutSeconds?: number;
|
||||
export type QueueConfig = {
|
||||
mode?: QueueMode;
|
||||
byProvider?: QueueModeByProvider;
|
||||
debounceMs?: number;
|
||||
cap?: number;
|
||||
drop?: QueueDropPolicy;
|
||||
};
|
||||
|
||||
export type AgentToolsConfig = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
sandbox?: {
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
};
|
||||
groupChat?: GroupChatConfig;
|
||||
/** Default agent id when no binding matches. Default: "main". */
|
||||
defaultAgentId?: string;
|
||||
};
|
||||
|
||||
export type ToolsConfig = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
agentToAgent?: {
|
||||
/** Enable agent-to-agent messaging tools. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Allowlist of agent ids or patterns (implementation-defined). */
|
||||
allow?: string[];
|
||||
};
|
||||
agents?: Record<
|
||||
string,
|
||||
{
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
/** Per-agent override for group mention patterns. */
|
||||
mentionPatterns?: string[];
|
||||
subagents?: {
|
||||
/** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */
|
||||
allowAgents?: string[];
|
||||
};
|
||||
sandbox?: {
|
||||
mode?: "off" | "non-main" | "all";
|
||||
/** Agent workspace access inside the sandbox. */
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
/** Container/workspace scope for sandbox isolation. */
|
||||
scope?: "session" | "agent" | "shared";
|
||||
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||
perSession?: boolean;
|
||||
workspaceRoot?: string;
|
||||
/** Docker-specific sandbox overrides for this agent. */
|
||||
docker?: SandboxDockerSettings;
|
||||
/** Optional sandboxed browser overrides for this agent. */
|
||||
browser?: SandboxBrowserSettings;
|
||||
/** Tool allow/deny policy for sandboxed sessions (deny wins). */
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
/** Auto-prune overrides for this agent. */
|
||||
prune?: SandboxPruneSettings;
|
||||
};
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
}
|
||||
>;
|
||||
bindings?: Array<{
|
||||
agentId: string;
|
||||
match: {
|
||||
provider: string;
|
||||
accountId?: string;
|
||||
peer?: { kind: "dm" | "group" | "channel"; id: string };
|
||||
guildId?: string;
|
||||
teamId?: string;
|
||||
/** Elevated bash permissions for the host machine. */
|
||||
elevated?: {
|
||||
/** Enable or disable elevated mode (default: true). */
|
||||
enabled?: boolean;
|
||||
/** Approved senders for /elevated (per-provider allowlists). */
|
||||
allowFrom?: AgentElevatedAllowFromConfig;
|
||||
};
|
||||
/** Bash tool defaults. */
|
||||
bash?: {
|
||||
/** Default time (ms) before a bash command auto-backgrounds. */
|
||||
backgroundMs?: number;
|
||||
/** Default timeout (seconds) before auto-killing bash commands. */
|
||||
timeoutSec?: number;
|
||||
/** How long to keep finished sessions in memory (ms). */
|
||||
cleanupMs?: number;
|
||||
};
|
||||
/** Sub-agent tool policy defaults (deny wins). */
|
||||
subagents?: {
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
}>;
|
||||
queue?: {
|
||||
mode?: QueueMode;
|
||||
byProvider?: QueueModeByProvider;
|
||||
debounceMs?: number;
|
||||
cap?: number;
|
||||
drop?: QueueDropPolicy;
|
||||
};
|
||||
/** Sandbox tool policy defaults (deny wins). */
|
||||
sandbox?: {
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentConfig = {
|
||||
id: string;
|
||||
default?: boolean;
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
identity?: IdentityConfig;
|
||||
groupChat?: GroupChatConfig;
|
||||
subagents?: {
|
||||
/** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */
|
||||
allowAgents?: string[];
|
||||
};
|
||||
sandbox?: {
|
||||
mode?: "off" | "non-main" | "all";
|
||||
/** Agent workspace access inside the sandbox. */
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
/**
|
||||
* Session tools visibility for sandboxed sessions.
|
||||
* - "spawned": only allow session tools to target sessions spawned from this session (default)
|
||||
* - "all": allow session tools to target any session
|
||||
*/
|
||||
sessionToolsVisibility?: "spawned" | "all";
|
||||
/** Container/workspace scope for sandbox isolation. */
|
||||
scope?: "session" | "agent" | "shared";
|
||||
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||
perSession?: boolean;
|
||||
workspaceRoot?: string;
|
||||
/** Docker-specific sandbox overrides for this agent. */
|
||||
docker?: SandboxDockerSettings;
|
||||
/** Optional sandboxed browser overrides for this agent. */
|
||||
browser?: SandboxBrowserSettings;
|
||||
/** Auto-prune overrides for this agent. */
|
||||
prune?: SandboxPruneSettings;
|
||||
};
|
||||
tools?: AgentToolsConfig;
|
||||
};
|
||||
|
||||
export type AgentsConfig = {
|
||||
defaults?: AgentDefaultsConfig;
|
||||
list?: AgentConfig[];
|
||||
};
|
||||
|
||||
export type AgentBinding = {
|
||||
agentId: string;
|
||||
match: {
|
||||
provider: string;
|
||||
accountId?: string;
|
||||
peer?: { kind: "dm" | "group" | "channel"; id: string };
|
||||
guildId?: string;
|
||||
teamId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AudioConfig = {
|
||||
transcription?: {
|
||||
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
|
||||
command: string[];
|
||||
timeoutSeconds?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type MessagesConfig = {
|
||||
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdbot]" if no allowFrom, else "")
|
||||
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
|
||||
groupChat?: GroupChatConfig;
|
||||
queue?: QueueConfig;
|
||||
/** Emoji reaction used to acknowledge inbound messages (empty disables). */
|
||||
ackReaction?: string;
|
||||
/** When to send ack reactions. Default: "group-mentions". */
|
||||
@@ -1097,6 +1153,113 @@ export type AgentContextPruningConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentDefaultsConfig = {
|
||||
/** Primary model and fallbacks (provider/model). */
|
||||
model?: AgentModelListConfig;
|
||||
/** Optional image-capable model and fallbacks (provider/model). */
|
||||
imageModel?: AgentModelListConfig;
|
||||
/** Model catalog with optional aliases (full provider/model keys). */
|
||||
models?: Record<string, AgentModelEntryConfig>;
|
||||
/** Agent working directory (preferred). Used as the default cwd for agent runs. */
|
||||
workspace?: string;
|
||||
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
||||
skipBootstrap?: boolean;
|
||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||
userTimezone?: string;
|
||||
/** Optional display-only context window override (used for % in status UIs). */
|
||||
contextTokens?: number;
|
||||
/** Opt-in: prune old tool results from the LLM context to reduce token usage. */
|
||||
contextPruning?: AgentContextPruningConfig;
|
||||
/** Default thinking level when no /think directive is present. */
|
||||
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high";
|
||||
/** Default verbose level when no /verbose directive is present. */
|
||||
verboseDefault?: "off" | "on";
|
||||
/** Default elevated level when no /elevated directive is present. */
|
||||
elevatedDefault?: "off" | "on";
|
||||
/** Default block streaming level when no override is present. */
|
||||
blockStreamingDefault?: "off" | "on";
|
||||
/**
|
||||
* Block streaming boundary:
|
||||
* - "text_end": end of each assistant text content block (before tool calls)
|
||||
* - "message_end": end of the whole assistant message (may include tool blocks)
|
||||
*/
|
||||
blockStreamingBreak?: "text_end" | "message_end";
|
||||
/** Soft block chunking for streamed replies (min/max chars, prefer paragraph/newline). */
|
||||
blockStreamingChunk?: {
|
||||
minChars?: number;
|
||||
maxChars?: number;
|
||||
breakPreference?: "paragraph" | "newline" | "sentence";
|
||||
};
|
||||
timeoutSeconds?: number;
|
||||
/** Max inbound media size in MB for agent-visible attachments (text note or future image attach). */
|
||||
mediaMaxMb?: number;
|
||||
typingIntervalSeconds?: number;
|
||||
/** Typing indicator start mode (never|instant|thinking|message). */
|
||||
typingMode?: TypingMode;
|
||||
/** Periodic background heartbeat runs. */
|
||||
heartbeat?: {
|
||||
/** Heartbeat interval (duration string, default unit: minutes; default: 30m). */
|
||||
every?: string;
|
||||
/** Heartbeat model override (provider/model). */
|
||||
model?: string;
|
||||
/** Delivery target (last|whatsapp|telegram|discord|signal|imessage|none). */
|
||||
target?:
|
||||
| "last"
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "none";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||
to?: string;
|
||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."). */
|
||||
prompt?: string;
|
||||
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||
ackMaxChars?: number;
|
||||
};
|
||||
/** Max concurrent agent runs across all conversations. Default: 1 (sequential). */
|
||||
maxConcurrent?: number;
|
||||
/** Sub-agent defaults (spawned via sessions_spawn). */
|
||||
subagents?: {
|
||||
/** Max concurrent sub-agent runs (global lane: "subagent"). Default: 1. */
|
||||
maxConcurrent?: number;
|
||||
/** Auto-archive sub-agent sessions after N minutes (default: 60). */
|
||||
archiveAfterMinutes?: number;
|
||||
};
|
||||
/** Optional sandbox settings for non-main sessions. */
|
||||
sandbox?: {
|
||||
/** Enable sandboxing for sessions. */
|
||||
mode?: "off" | "non-main" | "all";
|
||||
/**
|
||||
* Agent workspace access inside the sandbox.
|
||||
* - "none": do not mount the agent workspace into the container; use a sandbox workspace under workspaceRoot
|
||||
* - "ro": mount the agent workspace read-only; disables write/edit tools
|
||||
* - "rw": mount the agent workspace read/write; enables write/edit tools
|
||||
*/
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
/**
|
||||
* Session tools visibility for sandboxed sessions.
|
||||
* - "spawned": only allow session tools to target sessions spawned from this session (default)
|
||||
* - "all": allow session tools to target any session
|
||||
*/
|
||||
sessionToolsVisibility?: "spawned" | "all";
|
||||
/** Container/workspace scope for sandbox isolation. */
|
||||
scope?: "session" | "agent" | "shared";
|
||||
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||
perSession?: boolean;
|
||||
/** Root directory for sandbox workspaces. */
|
||||
workspaceRoot?: string;
|
||||
/** Docker-specific sandbox settings. */
|
||||
docker?: SandboxDockerSettings;
|
||||
/** Optional sandboxed browser settings. */
|
||||
browser?: SandboxBrowserSettings;
|
||||
/** Auto-prune sandbox containers. */
|
||||
prune?: SandboxPruneSettings;
|
||||
};
|
||||
};
|
||||
|
||||
export type ClawdbotConfig = {
|
||||
auth?: AuthConfig;
|
||||
env?: {
|
||||
@@ -1115,11 +1278,6 @@ export type ClawdbotConfig = {
|
||||
| { enabled?: boolean; timeoutMs?: number }
|
||||
| undefined;
|
||||
};
|
||||
identity?: {
|
||||
name?: string;
|
||||
theme?: string;
|
||||
emoji?: string;
|
||||
};
|
||||
wizard?: {
|
||||
lastRunAt?: string;
|
||||
lastRunVersion?: string;
|
||||
@@ -1135,145 +1293,10 @@ export type ClawdbotConfig = {
|
||||
};
|
||||
skills?: SkillsConfig;
|
||||
models?: ModelsConfig;
|
||||
agent?: {
|
||||
/** Primary model and fallbacks (provider/model). */
|
||||
model?: AgentModelListConfig;
|
||||
/** Optional image-capable model and fallbacks (provider/model). */
|
||||
imageModel?: AgentModelListConfig;
|
||||
/** Model catalog with optional aliases (full provider/model keys). */
|
||||
models?: Record<string, AgentModelEntryConfig>;
|
||||
/** Agent working directory (preferred). Used as the default cwd for agent runs. */
|
||||
workspace?: string;
|
||||
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
||||
skipBootstrap?: boolean;
|
||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||
userTimezone?: string;
|
||||
/** Optional display-only context window override (used for % in status UIs). */
|
||||
contextTokens?: number;
|
||||
/** Opt-in: prune old tool results from the LLM context to reduce token usage. */
|
||||
contextPruning?: AgentContextPruningConfig;
|
||||
/** Default thinking level when no /think directive is present. */
|
||||
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high";
|
||||
/** Default verbose level when no /verbose directive is present. */
|
||||
verboseDefault?: "off" | "on";
|
||||
/** Default elevated level when no /elevated directive is present. */
|
||||
elevatedDefault?: "off" | "on";
|
||||
/** Default block streaming level when no override is present. */
|
||||
blockStreamingDefault?: "off" | "on";
|
||||
/**
|
||||
* Block streaming boundary:
|
||||
* - "text_end": end of each assistant text content block (before tool calls)
|
||||
* - "message_end": end of the whole assistant message (may include tool blocks)
|
||||
*/
|
||||
blockStreamingBreak?: "text_end" | "message_end";
|
||||
/** Soft block chunking for streamed replies (min/max chars, prefer paragraph/newline). */
|
||||
blockStreamingChunk?: {
|
||||
minChars?: number;
|
||||
maxChars?: number;
|
||||
breakPreference?: "paragraph" | "newline" | "sentence";
|
||||
};
|
||||
timeoutSeconds?: number;
|
||||
/** Max inbound media size in MB for agent-visible attachments (text note or future image attach). */
|
||||
mediaMaxMb?: number;
|
||||
typingIntervalSeconds?: number;
|
||||
/** Typing indicator start mode (never|instant|thinking|message). */
|
||||
typingMode?: TypingMode;
|
||||
/** Periodic background heartbeat runs. */
|
||||
heartbeat?: {
|
||||
/** Heartbeat interval (duration string, default unit: minutes; default: 30m). */
|
||||
every?: string;
|
||||
/** Heartbeat model override (provider/model). */
|
||||
model?: string;
|
||||
/** Delivery target (last|whatsapp|telegram|discord|signal|imessage|msteams|none). */
|
||||
target?:
|
||||
| "last"
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "none";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||
to?: string;
|
||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."). */
|
||||
prompt?: string;
|
||||
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||
ackMaxChars?: number;
|
||||
};
|
||||
/** Max concurrent agent runs across all conversations. Default: 1 (sequential). */
|
||||
maxConcurrent?: number;
|
||||
/** Sub-agent defaults (spawned via sessions_spawn). */
|
||||
subagents?: {
|
||||
/** Max concurrent sub-agent runs (global lane: "subagent"). Default: 1. */
|
||||
maxConcurrent?: number;
|
||||
/** Auto-archive sub-agent sessions after N minutes (default: 60). */
|
||||
archiveAfterMinutes?: number;
|
||||
/** Tool allow/deny policy for sub-agent sessions (deny wins). */
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
};
|
||||
/** Bash tool defaults. */
|
||||
bash?: {
|
||||
/** Default time (ms) before a bash command auto-backgrounds. */
|
||||
backgroundMs?: number;
|
||||
/** Default timeout (seconds) before auto-killing bash commands. */
|
||||
timeoutSec?: number;
|
||||
/** How long to keep finished sessions in memory (ms). */
|
||||
cleanupMs?: number;
|
||||
};
|
||||
/** Elevated bash permissions for the host machine. */
|
||||
elevated?: {
|
||||
/** Enable or disable elevated mode (default: true). */
|
||||
enabled?: boolean;
|
||||
/** Approved senders for /elevated (per-provider allowlists). */
|
||||
allowFrom?: AgentElevatedAllowFromConfig;
|
||||
};
|
||||
/** Optional sandbox settings for non-main sessions. */
|
||||
sandbox?: {
|
||||
/** Enable sandboxing for sessions. */
|
||||
mode?: "off" | "non-main" | "all";
|
||||
/**
|
||||
* Agent workspace access inside the sandbox.
|
||||
* - "none": do not mount the agent workspace into the container; use a sandbox workspace under workspaceRoot
|
||||
* - "ro": mount the agent workspace read-only; disables write/edit tools
|
||||
* - "rw": mount the agent workspace read/write; enables write/edit tools
|
||||
*/
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
/**
|
||||
* Session tools visibility for sandboxed sessions.
|
||||
* - "spawned": only allow session tools to target sessions spawned from this session (default)
|
||||
* - "all": allow session tools to target any session
|
||||
*/
|
||||
sessionToolsVisibility?: "spawned" | "all";
|
||||
/** Container/workspace scope for sandbox isolation. */
|
||||
scope?: "session" | "agent" | "shared";
|
||||
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||
perSession?: boolean;
|
||||
/** Root directory for sandbox workspaces. */
|
||||
workspaceRoot?: string;
|
||||
/** Docker-specific sandbox settings. */
|
||||
docker?: SandboxDockerSettings;
|
||||
/** Optional sandboxed browser settings. */
|
||||
browser?: SandboxBrowserSettings;
|
||||
/** Tool allow/deny policy (deny wins). */
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
/** Auto-prune sandbox containers. */
|
||||
prune?: SandboxPruneSettings;
|
||||
};
|
||||
/** Global tool allow/deny policy for all providers (deny wins). */
|
||||
tools?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
};
|
||||
routing?: RoutingConfig;
|
||||
agents?: AgentsConfig;
|
||||
tools?: ToolsConfig;
|
||||
bindings?: AgentBinding[];
|
||||
audio?: AudioConfig;
|
||||
messages?: MessagesConfig;
|
||||
commands?: CommandsConfig;
|
||||
session?: SessionConfig;
|
||||
|
||||
@@ -2,11 +2,7 @@ import {
|
||||
findDuplicateAgentDirs,
|
||||
formatDuplicateAgentDirError,
|
||||
} from "./agent-dirs.js";
|
||||
import {
|
||||
applyIdentityDefaults,
|
||||
applyModelDefaults,
|
||||
applySessionDefaults,
|
||||
} from "./defaults.js";
|
||||
import { applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js";
|
||||
import { ClawdbotSchema } from "./zod-schema.js";
|
||||
@@ -42,7 +38,7 @@ export function validateConfigObject(
|
||||
ok: false,
|
||||
issues: [
|
||||
{
|
||||
path: "routing.agents",
|
||||
path: "agents.list",
|
||||
message: formatDuplicateAgentDirError(duplicates),
|
||||
},
|
||||
],
|
||||
@@ -51,9 +47,7 @@ export function validateConfigObject(
|
||||
return {
|
||||
ok: true,
|
||||
config: applyModelDefaults(
|
||||
applySessionDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
applySessionDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
+287
-263
@@ -61,6 +61,14 @@ const GroupChatSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const IdentitySchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const QueueModeSchema = z.union([
|
||||
z.literal("steer"),
|
||||
z.literal("followup"),
|
||||
@@ -133,6 +141,16 @@ const QueueModeBySurfaceSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const QueueSchema = z
|
||||
.object({
|
||||
mode: QueueModeSchema.optional(),
|
||||
byProvider: QueueModeBySurfaceSchema,
|
||||
debounceMs: z.number().int().nonnegative().optional(),
|
||||
cap: z.number().int().positive().optional(),
|
||||
drop: QueueDropSchema.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const TranscribeAudioSchema = z
|
||||
.object({
|
||||
command: z.array(z.string()),
|
||||
@@ -554,6 +572,8 @@ const MessagesSchema = z
|
||||
.object({
|
||||
messagePrefix: z.string().optional(),
|
||||
responsePrefix: z.string().optional(),
|
||||
groupChat: GroupChatSchema,
|
||||
queue: QueueSchema,
|
||||
ackReaction: z.string().optional(),
|
||||
ackReactionScope: z
|
||||
.enum(["group-mentions", "group-all", "direct", "all"])
|
||||
@@ -667,96 +687,140 @@ const ToolPolicySchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const RoutingSchema = z
|
||||
const ElevatedAllowFromSchema = z
|
||||
.object({
|
||||
groupChat: GroupChatSchema,
|
||||
transcribeAudio: TranscribeAudioSchema,
|
||||
defaultAgentId: z.string().optional(),
|
||||
whatsapp: z.array(z.string()).optional(),
|
||||
telegram: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
discord: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
slack: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
signal: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
imessage: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
webchat: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentSandboxSchema = z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
sessionToolsVisibility: z
|
||||
.union([z.literal("spawned"), z.literal("all")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([z.literal("session"), z.literal("agent"), z.literal("shared")])
|
||||
.optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
browser: SandboxBrowserSchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentToolsSchema = z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
default: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
identity: IdentitySchema,
|
||||
groupChat: GroupChatSchema,
|
||||
subagents: z
|
||||
.object({
|
||||
allowAgents: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
sandbox: AgentSandboxSchema,
|
||||
tools: AgentToolsSchema,
|
||||
});
|
||||
|
||||
const ToolsSchema = z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
agentToAgent: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
agents: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
mentionPatterns: z.array(z.string()).optional(),
|
||||
subagents: z
|
||||
.object({
|
||||
allowAgents: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("non-main"),
|
||||
z.literal("all"),
|
||||
])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([
|
||||
z.literal("session"),
|
||||
z.literal("agent"),
|
||||
z.literal("shared"),
|
||||
])
|
||||
.optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
browser: SandboxBrowserSchema,
|
||||
tools: ToolPolicySchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.optional(),
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
bindings: z
|
||||
.array(
|
||||
z.object({
|
||||
agentId: z.string(),
|
||||
match: z.object({
|
||||
provider: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
peer: z
|
||||
.object({
|
||||
kind: z.union([
|
||||
z.literal("dm"),
|
||||
z.literal("group"),
|
||||
z.literal("channel"),
|
||||
]),
|
||||
id: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
guildId: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
queue: z
|
||||
elevated: z
|
||||
.object({
|
||||
mode: QueueModeSchema.optional(),
|
||||
byProvider: QueueModeBySurfaceSchema,
|
||||
debounceMs: z.number().int().nonnegative().optional(),
|
||||
cap: z.number().int().positive().optional(),
|
||||
drop: QueueDropSchema.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: ElevatedAllowFromSchema,
|
||||
})
|
||||
.optional(),
|
||||
bash: z
|
||||
.object({
|
||||
backgroundMs: z.number().int().positive().optional(),
|
||||
timeoutSec: z.number().int().positive().optional(),
|
||||
cleanupMs: z.number().int().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
subagents: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentsSchema = z
|
||||
.object({
|
||||
defaults: z.lazy(() => AgentDefaultsSchema).optional(),
|
||||
list: z.array(AgentEntrySchema).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const BindingsSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
agentId: z.string(),
|
||||
match: z.object({
|
||||
provider: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
peer: z
|
||||
.object({
|
||||
kind: z.union([
|
||||
z.literal("dm"),
|
||||
z.literal("group"),
|
||||
z.literal("channel"),
|
||||
]),
|
||||
id: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
guildId: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.optional();
|
||||
|
||||
const AudioSchema = z
|
||||
.object({
|
||||
transcription: TranscribeAudioSchema,
|
||||
})
|
||||
.optional();
|
||||
|
||||
@@ -832,6 +896,145 @@ const HooksGmailSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentDefaultsSchema = z
|
||||
.object({
|
||||
model: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
imageModel: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
models: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
alias: z.string().optional(),
|
||||
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
||||
params: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
workspace: z.string().optional(),
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
userTimezone: z.string().optional(),
|
||||
contextTokens: z.number().int().positive().optional(),
|
||||
contextPruning: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("adaptive"),
|
||||
z.literal("aggressive"),
|
||||
])
|
||||
.optional(),
|
||||
keepLastAssistants: z.number().int().nonnegative().optional(),
|
||||
softTrimRatio: z.number().min(0).max(1).optional(),
|
||||
hardClearRatio: z.number().min(0).max(1).optional(),
|
||||
minPrunableToolChars: z.number().int().nonnegative().optional(),
|
||||
tools: z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
softTrim: z
|
||||
.object({
|
||||
maxChars: z.number().int().nonnegative().optional(),
|
||||
headChars: z.number().int().nonnegative().optional(),
|
||||
tailChars: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.optional(),
|
||||
hardClear: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
thinkingDefault: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("minimal"),
|
||||
z.literal("low"),
|
||||
z.literal("medium"),
|
||||
z.literal("high"),
|
||||
])
|
||||
.optional(),
|
||||
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
blockStreamingDefault: z
|
||||
.union([z.literal("off"), z.literal("on")])
|
||||
.optional(),
|
||||
blockStreamingBreak: z
|
||||
.union([z.literal("text_end"), z.literal("message_end")])
|
||||
.optional(),
|
||||
blockStreamingChunk: z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([
|
||||
z.literal("paragraph"),
|
||||
z.literal("newline"),
|
||||
z.literal("sentence"),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
typingIntervalSeconds: z.number().int().positive().optional(),
|
||||
typingMode: z
|
||||
.union([
|
||||
z.literal("never"),
|
||||
z.literal("instant"),
|
||||
z.literal("thinking"),
|
||||
z.literal("message"),
|
||||
])
|
||||
.optional(),
|
||||
heartbeat: HeartbeatSchema,
|
||||
maxConcurrent: z.number().int().positive().optional(),
|
||||
subagents: z
|
||||
.object({
|
||||
maxConcurrent: z.number().int().positive().optional(),
|
||||
archiveAfterMinutes: z.number().int().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
sessionToolsVisibility: z
|
||||
.union([z.literal("spawned"), z.literal("all")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([
|
||||
z.literal("session"),
|
||||
z.literal("agent"),
|
||||
z.literal("shared"),
|
||||
])
|
||||
.optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
browser: SandboxBrowserSchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const ClawdbotSchema = z.object({
|
||||
env: z
|
||||
.object({
|
||||
@@ -845,13 +1048,6 @@ export const ClawdbotSchema = z.object({
|
||||
})
|
||||
.catchall(z.string())
|
||||
.optional(),
|
||||
identity: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
wizard: z
|
||||
.object({
|
||||
lastRunAt: z.string().optional(),
|
||||
@@ -954,182 +1150,10 @@ export const ClawdbotSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
models: ModelsConfigSchema,
|
||||
agent: z
|
||||
.object({
|
||||
model: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
imageModel: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
models: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
alias: z.string().optional(),
|
||||
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
||||
params: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
workspace: z.string().optional(),
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
userTimezone: z.string().optional(),
|
||||
contextTokens: z.number().int().positive().optional(),
|
||||
contextPruning: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("adaptive"),
|
||||
z.literal("aggressive"),
|
||||
])
|
||||
.optional(),
|
||||
keepLastAssistants: z.number().int().nonnegative().optional(),
|
||||
softTrimRatio: z.number().min(0).max(1).optional(),
|
||||
hardClearRatio: z.number().min(0).max(1).optional(),
|
||||
minPrunableToolChars: z.number().int().nonnegative().optional(),
|
||||
tools: z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
softTrim: z
|
||||
.object({
|
||||
maxChars: z.number().int().nonnegative().optional(),
|
||||
headChars: z.number().int().nonnegative().optional(),
|
||||
tailChars: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.optional(),
|
||||
hardClear: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
tools: z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
thinkingDefault: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("minimal"),
|
||||
z.literal("low"),
|
||||
z.literal("medium"),
|
||||
z.literal("high"),
|
||||
])
|
||||
.optional(),
|
||||
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
blockStreamingDefault: z
|
||||
.union([z.literal("off"), z.literal("on")])
|
||||
.optional(),
|
||||
blockStreamingBreak: z
|
||||
.union([z.literal("text_end"), z.literal("message_end")])
|
||||
.optional(),
|
||||
blockStreamingChunk: z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([
|
||||
z.literal("paragraph"),
|
||||
z.literal("newline"),
|
||||
z.literal("sentence"),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
typingIntervalSeconds: z.number().int().positive().optional(),
|
||||
typingMode: z
|
||||
.union([
|
||||
z.literal("never"),
|
||||
z.literal("instant"),
|
||||
z.literal("thinking"),
|
||||
z.literal("message"),
|
||||
])
|
||||
.optional(),
|
||||
heartbeat: HeartbeatSchema,
|
||||
maxConcurrent: z.number().int().positive().optional(),
|
||||
subagents: z
|
||||
.object({
|
||||
maxConcurrent: z.number().int().positive().optional(),
|
||||
archiveAfterMinutes: z.number().int().positive().optional(),
|
||||
tools: z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
bash: z
|
||||
.object({
|
||||
backgroundMs: z.number().int().positive().optional(),
|
||||
timeoutSec: z.number().int().positive().optional(),
|
||||
cleanupMs: z.number().int().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
elevated: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z
|
||||
.object({
|
||||
whatsapp: z.array(z.string()).optional(),
|
||||
telegram: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
discord: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
slack: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
signal: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
imessage: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
msteams: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
webchat: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
sessionToolsVisibility: z
|
||||
.union([z.literal("spawned"), z.literal("all")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([
|
||||
z.literal("session"),
|
||||
z.literal("agent"),
|
||||
z.literal("shared"),
|
||||
])
|
||||
.optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
browser: SandboxBrowserSchema,
|
||||
tools: ToolPolicySchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
routing: RoutingSchema,
|
||||
agents: AgentsSchema,
|
||||
tools: ToolsSchema,
|
||||
bindings: BindingsSchema,
|
||||
audio: AudioSchema,
|
||||
messages: MessagesSchema,
|
||||
commands: CommandsSchema,
|
||||
session: SessionSchema,
|
||||
|
||||
@@ -63,9 +63,11 @@ function makeCfg(
|
||||
overrides: Partial<ClawdbotConfig> = {},
|
||||
): ClawdbotConfig {
|
||||
const base: ClawdbotConfig = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
session: { store: storePath, mainKey: "main" },
|
||||
} as ClawdbotConfig;
|
||||
@@ -738,7 +740,13 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
});
|
||||
|
||||
const cfg = makeCfg(home, storePath);
|
||||
cfg.agent = { ...cfg.agent, heartbeat: { ackMaxChars: 0 } };
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
heartbeat: { ackMaxChars: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const res = await runCronIsolatedAgentTurn({
|
||||
cfg,
|
||||
|
||||
@@ -269,12 +269,11 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
sessionKey: string;
|
||||
lane?: string;
|
||||
}): Promise<RunCronAgentTurnResult> {
|
||||
const agentCfg = params.cfg.agent;
|
||||
const workspaceDirRaw =
|
||||
params.cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const agentCfg = params.cfg.agents?.defaults;
|
||||
const workspaceDirRaw = agentCfg?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !params.cfg.agent?.skipBootstrap,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
|
||||
@@ -521,7 +520,8 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
// This allows cron jobs to silently ack when nothing to report but still deliver
|
||||
// actual content when there is something to say.
|
||||
const ackMaxChars =
|
||||
params.cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
|
||||
params.cfg.agents?.defaults?.heartbeat?.ackMaxChars ??
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
|
||||
const skipHeartbeatDelivery =
|
||||
delivery && isHeartbeatOnlyResponse(payloads, Math.max(0, ackMaxChars));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChannelType, MessageType } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMock = vi.fn();
|
||||
const reactMock = vi.fn();
|
||||
const updateLastRouteMock = vi.fn();
|
||||
const dispatchMock = vi.fn();
|
||||
const readAllowFromStoreMock = vi.fn();
|
||||
@@ -10,6 +11,9 @@ const upsertPairingRequestMock = vi.fn();
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
|
||||
reactMessageDiscord: async (...args: unknown[]) => {
|
||||
reactMock(...args);
|
||||
},
|
||||
}));
|
||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
||||
@@ -48,11 +52,15 @@ describe("discord tool result dispatch", () => {
|
||||
it("sends status replies with responsePrefix", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: { dm: { enabled: true, policy: "open" } },
|
||||
routing: { allowFrom: [] },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const runtimeError = vi.fn();
|
||||
@@ -114,10 +122,14 @@ describe("discord tool result dispatch", () => {
|
||||
it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
discord: { dm: { enabled: true, policy: "pairing", allowFrom: [] } },
|
||||
routing: { allowFrom: [] },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
@@ -184,15 +196,19 @@ describe("discord tool result dispatch", () => {
|
||||
it("accepts guild messages when mentionPatterns match", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: { "*": { requireMention: true } },
|
||||
},
|
||||
routing: {
|
||||
allowFrom: [],
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
groupChat: { mentionPatterns: ["\\bclawd\\b"] },
|
||||
},
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
@@ -271,14 +287,18 @@ describe("discord tool result dispatch", () => {
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: { "*": { requireMention: false } },
|
||||
},
|
||||
routing: { allowFrom: [] },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
@@ -377,19 +397,21 @@ describe("discord tool result dispatch", () => {
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: { "*": { requireMention: false } },
|
||||
},
|
||||
routing: {
|
||||
allowFrom: [],
|
||||
bindings: [
|
||||
{ agentId: "support", match: { provider: "discord", guildId: "g1" } },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "support", match: { provider: "discord", guildId: "g1" } },
|
||||
],
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
|
||||
@@ -17,6 +17,7 @@ import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
||||
import type { APIAttachment } from "discord-api-types/v10";
|
||||
import { ApplicationCommandOptionType, Routes } from "discord-api-types/v10";
|
||||
|
||||
import { resolveAckReaction } from "../agents/identity.js";
|
||||
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import {
|
||||
@@ -501,7 +502,6 @@ export function createDiscordMessageHandler(params: {
|
||||
guildEntries,
|
||||
} = params;
|
||||
const logger = getChildLogger({ module: "discord-auto-reply" });
|
||||
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const groupPolicy = discordConfig?.groupPolicy ?? "open";
|
||||
|
||||
@@ -842,6 +842,7 @@ export function createDiscordMessageHandler(params: {
|
||||
logVerbose(`discord: drop message ${message.id} (empty content)`);
|
||||
return;
|
||||
}
|
||||
const ackReaction = resolveAckReaction(cfg, route.agentId);
|
||||
const shouldAckReaction = () => {
|
||||
if (!ackReaction) return false;
|
||||
if (ackReactionScope === "all") return true;
|
||||
|
||||
@@ -14,10 +14,10 @@ describe("diffConfigPaths", () => {
|
||||
});
|
||||
|
||||
it("captures array changes", () => {
|
||||
const prev = { routing: { groupChat: { mentionPatterns: ["a"] } } };
|
||||
const next = { routing: { groupChat: { mentionPatterns: ["b"] } } };
|
||||
const prev = { messages: { groupChat: { mentionPatterns: ["a"] } } };
|
||||
const next = { messages: { groupChat: { mentionPatterns: ["b"] } } };
|
||||
const paths = diffConfigPaths(prev, next);
|
||||
expect(paths).toContain("routing.groupChat.mentionPatterns");
|
||||
expect(paths).toContain("messages.groupChat.mentionPatterns");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -64,7 +64,11 @@ const RELOAD_RULES: ReloadRule[] = [
|
||||
{ prefix: "gateway.reload", kind: "none" },
|
||||
{ prefix: "hooks.gmail", kind: "hot", actions: ["restart-gmail-watcher"] },
|
||||
{ prefix: "hooks", kind: "hot", actions: ["reload-hooks"] },
|
||||
{ prefix: "agent.heartbeat", kind: "hot", actions: ["restart-heartbeat"] },
|
||||
{
|
||||
prefix: "agents.defaults.heartbeat",
|
||||
kind: "hot",
|
||||
actions: ["restart-heartbeat"],
|
||||
},
|
||||
{ prefix: "cron", kind: "hot", actions: ["restart-cron"] },
|
||||
{
|
||||
prefix: "browser",
|
||||
@@ -78,12 +82,13 @@ const RELOAD_RULES: ReloadRule[] = [
|
||||
{ prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] },
|
||||
{ prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
|
||||
{ prefix: "msteams", kind: "hot", actions: ["restart-provider:msteams"] },
|
||||
{ prefix: "identity", kind: "none" },
|
||||
{ prefix: "agents", kind: "none" },
|
||||
{ prefix: "tools", kind: "none" },
|
||||
{ prefix: "bindings", kind: "none" },
|
||||
{ prefix: "audio", kind: "none" },
|
||||
{ prefix: "wizard", kind: "none" },
|
||||
{ prefix: "logging", kind: "none" },
|
||||
{ prefix: "models", kind: "none" },
|
||||
{ prefix: "agent", kind: "none" },
|
||||
{ prefix: "routing", kind: "none" },
|
||||
{ prefix: "messages", kind: "none" },
|
||||
{ prefix: "session", kind: "none" },
|
||||
{ prefix: "whatsapp", kind: "none" },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user