fix: enforce strict config validation
This commit is contained in:
@@ -12,7 +12,7 @@ describe("config env vars", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
env: { OPENROUTER_API_KEY: "config-key" },
|
||||
env: { vars: { OPENROUTER_API_KEY: "config-key" } },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -36,7 +36,7 @@ describe("config env vars", () => {
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
env: { OPENROUTER_API_KEY: "config-key" },
|
||||
env: { vars: { OPENROUTER_API_KEY: "config-key" } },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@@ -164,8 +164,6 @@ describe("config identity defaults", () => {
|
||||
messages: {
|
||||
messagePrefix: "[clawdbot]",
|
||||
responsePrefix: "🦞",
|
||||
// legacy field should be ignored (moved to providers)
|
||||
textChunkLimit: 9999,
|
||||
},
|
||||
channels: {
|
||||
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("legacy config detection", () => {
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const res = validateConfigObject({
|
||||
channels: { imessage: { cliPath: "imsg; rm -rf /" } },
|
||||
tools: { audio: { transcription: { args: ["--model", "base"] } } },
|
||||
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
@@ -76,7 +76,7 @@ describe("legacy config detection", () => {
|
||||
vi.resetModules();
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const res = validateConfigObject({
|
||||
tools: { audio: { transcription: { args: ["--model", "base"] } } },
|
||||
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
@@ -85,11 +85,9 @@ describe("legacy config detection", () => {
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const res = validateConfigObject({
|
||||
channels: { imessage: { cliPath: "/Applications/Imsg Tools/imsg" } },
|
||||
tools: {
|
||||
audio: {
|
||||
transcription: {
|
||||
args: ["--model"],
|
||||
},
|
||||
audio: {
|
||||
transcription: {
|
||||
command: ["whisper", "--model"],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -166,7 +164,7 @@ describe("legacy config detection", () => {
|
||||
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||
expect(res.config?.agent).toBeUndefined();
|
||||
});
|
||||
it("auto-migrates legacy config in snapshot (no legacyIssues)", async () => {
|
||||
it("flags legacy config in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -176,31 +174,23 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.length).toBe(0);
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
||||
routing?: unknown;
|
||||
};
|
||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(parsed.routing).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
routing?: { allowFrom?: string[] };
|
||||
channels?: unknown;
|
||||
};
|
||||
expect(parsed.routing?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
});
|
||||
});
|
||||
it("auto-migrates claude-cli auth profile mode to oauth", async () => {
|
||||
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -220,27 +210,19 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
auth?: { profiles?: Record<string, { mode?: string }> };
|
||||
};
|
||||
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
auth?: { profiles?: Record<string, { mode?: string }> };
|
||||
};
|
||||
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||
});
|
||||
});
|
||||
it("auto-migrates legacy provider sections on load and writes back", async () => {
|
||||
it("flags legacy provider sections in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -250,29 +232,23 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(cfg.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
||||
whatsapp?: unknown;
|
||||
};
|
||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
||||
expect(parsed.whatsapp).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
channels?: unknown;
|
||||
whatsapp?: unknown;
|
||||
};
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
expect(parsed.whatsapp).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it("auto-migrates routing.allowFrom on load and writes back", async () => {
|
||||
it("flags routing.allowFrom in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -282,26 +258,23 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(cfg.channels?.whatsapp?.allowFrom).toEqual(["+1666"]);
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
||||
routing?: unknown;
|
||||
};
|
||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+1666"]);
|
||||
expect(parsed.routing).toBeUndefined();
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
channels?: unknown;
|
||||
routing?: { allowFrom?: string[] };
|
||||
};
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
expect(parsed.routing?.allowFrom).toEqual(["+1666"]);
|
||||
});
|
||||
});
|
||||
it("auto-migrates bindings[].match.provider on load and writes back", async () => {
|
||||
it("rejects bindings[].match.provider on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -317,28 +290,21 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.bindings?.[0]?.match?.channel).toBe("slack");
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
bindings?: Array<{ match?: { channel?: string; provider?: string } }>;
|
||||
};
|
||||
expect(parsed.bindings?.[0]?.match?.channel).toBe("slack");
|
||||
expect(parsed.bindings?.[0]?.match?.provider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
bindings?: Array<{ match?: { provider?: string } }>;
|
||||
};
|
||||
expect(parsed.bindings?.[0]?.match?.provider).toBe("slack");
|
||||
});
|
||||
});
|
||||
it("auto-migrates bindings[].match.accountID on load and writes back", async () => {
|
||||
it("rejects bindings[].match.accountID on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -354,28 +320,21 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.bindings?.[0]?.match?.accountId).toBe("work");
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
bindings?: Array<{ match?: { accountId?: string; accountID?: string } }>;
|
||||
};
|
||||
expect(parsed.bindings?.[0]?.match?.accountId).toBe("work");
|
||||
expect(parsed.bindings?.[0]?.match?.accountID).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
bindings?: Array<{ match?: { accountID?: string } }>;
|
||||
};
|
||||
expect(parsed.bindings?.[0]?.match?.accountID).toBe("work");
|
||||
});
|
||||
});
|
||||
it("auto-migrates session.sendPolicy.rules[].match.provider on load and writes back", async () => {
|
||||
it("rejects session.sendPolicy.rules[].match.provider on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -395,34 +354,21 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
session?: {
|
||||
sendPolicy?: {
|
||||
rules?: Array<{
|
||||
match?: { channel?: string; provider?: string };
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
|
||||
};
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBe("telegram");
|
||||
});
|
||||
});
|
||||
it("auto-migrates messages.queue.byProvider on load and writes back", async () => {
|
||||
it("rejects messages.queue.byProvider on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -432,30 +378,22 @@ describe("legacy config detection", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.resetModules();
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.messages?.queue?.byChannel?.whatsapp).toBe("queue");
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
messages?: {
|
||||
queue?: {
|
||||
byChannel?: Record<string, unknown>;
|
||||
byProvider?: unknown;
|
||||
};
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
messages?: {
|
||||
queue?: {
|
||||
byProvider?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
expect(parsed.messages?.queue?.byChannel?.whatsapp).toBe("queue");
|
||||
expect(parsed.messages?.queue?.byProvider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
};
|
||||
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("multi-agent agentDir validation", () => {
|
||||
{ id: "b", agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||
],
|
||||
},
|
||||
bindings: [{ agentId: "a", match: { provider: "telegram" } }],
|
||||
bindings: [{ agentId: "a", match: { channel: "telegram" } }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@@ -3,21 +3,18 @@ import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
|
||||
describe("config preservation on validation failure", () => {
|
||||
it("preserves unknown fields via passthrough", async () => {
|
||||
describe("config strict validation", () => {
|
||||
it("rejects unknown fields", async () => {
|
||||
vi.resetModules();
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const res = validateConfigObject({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
customUnknownField: { nested: "value" },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect((res as { config: Record<string, unknown> }).config.customUnknownField).toEqual({
|
||||
nested: "value",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves config data when validation fails", async () => {
|
||||
it("flags legacy config entries without auto-migrating", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -26,7 +23,6 @@ describe("config preservation on validation failure", () => {
|
||||
JSON.stringify({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
routing: { allowFrom: ["+15555550123"] },
|
||||
customData: { preserved: true },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
@@ -35,12 +31,8 @@ describe("config preservation on validation failure", () => {
|
||||
const { readConfigFileSnapshot } = await import("./config.js");
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues).toHaveLength(0);
|
||||
expect((snap.config as Record<string, unknown>).customData).toEqual({
|
||||
preserved: true,
|
||||
});
|
||||
expect(snap.config.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.legacyIssues).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
113
src/config/io.ts
113
src/config/io.ts
@@ -24,10 +24,9 @@ import {
|
||||
import { VERSION } from "../version.js";
|
||||
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
||||
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
||||
import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "./paths.js";
|
||||
import { applyPluginAutoEnable } from "./plugin-auto-enable.js";
|
||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||
import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
@@ -87,29 +86,6 @@ function coerceConfig(value: unknown): ClawdbotConfig {
|
||||
return value as ClawdbotConfig;
|
||||
}
|
||||
|
||||
function rotateConfigBackupsSync(configPath: string, ioFs: typeof fs): void {
|
||||
if (CONFIG_BACKUP_COUNT <= 1) return;
|
||||
const backupBase = `${configPath}.bak`;
|
||||
const maxIndex = CONFIG_BACKUP_COUNT - 1;
|
||||
try {
|
||||
ioFs.unlinkSync(`${backupBase}.${maxIndex}`);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
for (let index = maxIndex - 1; index >= 1; index -= 1) {
|
||||
try {
|
||||
ioFs.renameSync(`${backupBase}.${index}`, `${backupBase}.${index + 1}`);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
try {
|
||||
ioFs.renameSync(backupBase, `${backupBase}.1`);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateConfigBackups(configPath: string, ioFs: typeof fs.promises): Promise<void> {
|
||||
if (CONFIG_BACKUP_COUNT <= 1) return;
|
||||
const backupBase = `${configPath}.bak`;
|
||||
@@ -147,10 +123,6 @@ function warnOnConfigMiskeys(raw: unknown, logger: Pick<typeof console, "warn">)
|
||||
}
|
||||
}
|
||||
|
||||
function formatLegacyMigrationLog(changes: string[]): string {
|
||||
return `Auto-migrated config:\n${changes.map((entry) => `- ${entry}`).join("\n")}`;
|
||||
}
|
||||
|
||||
function stampConfigVersion(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
@@ -231,56 +203,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const deps = normalizeDeps(overrides);
|
||||
const configPath = resolveConfigPathForDeps(deps);
|
||||
|
||||
const writeConfigFileSync = (cfg: ClawdbotConfig) => {
|
||||
const dir = path.dirname(configPath);
|
||||
deps.fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
|
||||
const tmp = path.join(
|
||||
dir,
|
||||
`${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`,
|
||||
);
|
||||
|
||||
deps.fs.writeFileSync(tmp, json, { encoding: "utf-8", mode: 0o600 });
|
||||
|
||||
if (deps.fs.existsSync(configPath)) {
|
||||
rotateConfigBackupsSync(configPath, deps.fs);
|
||||
try {
|
||||
deps.fs.copyFileSync(configPath, `${configPath}.bak`);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
deps.fs.renameSync(tmp, configPath);
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "EPERM" || code === "EEXIST") {
|
||||
deps.fs.copyFileSync(tmp, configPath);
|
||||
try {
|
||||
deps.fs.chmodSync(configPath, 0o600);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
try {
|
||||
deps.fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
deps.fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
function loadConfig(): ClawdbotConfig {
|
||||
try {
|
||||
if (!deps.fs.existsSync(configPath)) {
|
||||
@@ -307,14 +229,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
// Substitute ${VAR} env var references
|
||||
const substituted = resolveConfigEnvVars(resolved, deps.env);
|
||||
|
||||
const migrated = applyLegacyMigrations(substituted);
|
||||
let resolvedConfig = migrated.next ?? substituted;
|
||||
const autoEnable = applyPluginAutoEnable({
|
||||
config: coerceConfig(resolvedConfig),
|
||||
env: deps.env,
|
||||
});
|
||||
resolvedConfig = autoEnable.config;
|
||||
const migrationChanges = [...migrated.changes, ...autoEnable.changes];
|
||||
const resolvedConfig = substituted;
|
||||
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
||||
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
|
||||
const validated = ClawdbotSchema.safeParse(resolvedConfig);
|
||||
@@ -326,14 +241,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
return {};
|
||||
}
|
||||
warnIfConfigFromFuture(validated.data as ClawdbotConfig, deps.logger);
|
||||
if (migrationChanges.length > 0) {
|
||||
deps.logger.warn(formatLegacyMigrationLog(migrationChanges));
|
||||
try {
|
||||
writeConfigFileSync(resolvedConfig as ClawdbotConfig);
|
||||
} catch (err) {
|
||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
const cfg = applyModelDefaults(
|
||||
applyCompactionDefaults(
|
||||
applyContextPruningDefaults(
|
||||
@@ -467,14 +374,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const migrated = applyLegacyMigrations(substituted);
|
||||
let resolvedConfigRaw = migrated.next ?? substituted;
|
||||
const autoEnable = applyPluginAutoEnable({
|
||||
config: coerceConfig(resolvedConfigRaw),
|
||||
env: deps.env,
|
||||
});
|
||||
resolvedConfigRaw = autoEnable.config;
|
||||
const migrationChanges = [...migrated.changes, ...autoEnable.changes];
|
||||
const resolvedConfigRaw = substituted;
|
||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
|
||||
|
||||
const validated = validateConfigObject(resolvedConfigRaw);
|
||||
@@ -493,13 +393,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}
|
||||
|
||||
warnIfConfigFromFuture(validated.config, deps.logger);
|
||||
if (migrationChanges.length > 0) {
|
||||
deps.logger.warn(formatLegacyMigrationLog(migrationChanges));
|
||||
await writeConfigFile(validated.config).catch((err) => {
|
||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
path: configPath,
|
||||
exists: true,
|
||||
|
||||
@@ -20,21 +20,25 @@ export const AgentDefaultsSchema = z
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
imageModel: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.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(),
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
workspace: z.string().optional(),
|
||||
@@ -62,6 +66,7 @@ export const AgentDefaultsSchema = z
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
softTrim: z
|
||||
.object({
|
||||
@@ -69,14 +74,17 @@ export const AgentDefaultsSchema = z
|
||||
headChars: z.number().int().nonnegative().optional(),
|
||||
tailChars: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
hardClear: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
compaction: z
|
||||
.object({
|
||||
@@ -89,8 +97,10 @@ export const AgentDefaultsSchema = z
|
||||
prompt: z.string().optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
thinkingDefault: z
|
||||
.union([
|
||||
@@ -132,10 +142,11 @@ export const AgentDefaultsSchema = z
|
||||
z.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
}),
|
||||
}).strict(),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
@@ -149,6 +160,8 @@ export const AgentDefaultsSchema = z
|
||||
browser: SandboxBrowserSchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -30,6 +30,7 @@ export const HeartbeatSchema = z
|
||||
prompt: z.string().optional(),
|
||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val.every) return;
|
||||
try {
|
||||
@@ -69,7 +70,7 @@ export const SandboxDockerSchema = z
|
||||
z.object({
|
||||
soft: z.number().int().nonnegative().optional(),
|
||||
hard: z.number().int().nonnegative().optional(),
|
||||
}),
|
||||
}).strict(),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
@@ -79,6 +80,7 @@ export const SandboxDockerSchema = z
|
||||
extraHosts: z.array(z.string()).optional(),
|
||||
binds: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const SandboxBrowserSchema = z
|
||||
@@ -98,6 +100,7 @@ export const SandboxBrowserSchema = z
|
||||
autoStart: z.boolean().optional(),
|
||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const SandboxPruneSchema = z
|
||||
@@ -105,6 +108,7 @@ export const SandboxPruneSchema = z
|
||||
idleHours: z.number().int().nonnegative().optional(),
|
||||
maxAgeDays: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolPolicySchema = z
|
||||
@@ -112,6 +116,7 @@ export const ToolPolicySchema = z
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsWebSearchSchema = z
|
||||
@@ -123,6 +128,7 @@ export const ToolsWebSearchSchema = z
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsWebFetchSchema = z
|
||||
@@ -133,6 +139,7 @@ export const ToolsWebFetchSchema = z
|
||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsWebSchema = z
|
||||
@@ -140,17 +147,20 @@ export const ToolsWebSchema = z
|
||||
search: ToolsWebSearchSchema,
|
||||
fetch: ToolsWebFetchSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolProfileSchema = z
|
||||
.union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")])
|
||||
.optional();
|
||||
|
||||
export const ToolPolicyWithProfileSchema = z.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
profile: ToolProfileSchema,
|
||||
});
|
||||
export const ToolPolicyWithProfileSchema = z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
profile: ToolProfileSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// Provider docking: allowlists keyed by provider id (no schema updates when adding providers).
|
||||
export const ElevatedAllowFromSchema = z
|
||||
@@ -169,6 +179,7 @@ export const AgentSandboxSchema = z
|
||||
browser: SandboxBrowserSchema,
|
||||
prune: SandboxPruneSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const AgentToolsSchema = z
|
||||
@@ -182,6 +193,7 @@ export const AgentToolsSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: ElevatedAllowFromSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
exec: z
|
||||
.object({
|
||||
@@ -199,15 +211,19 @@ export const AgentToolsSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allowModels: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const MemorySearchSchema = z
|
||||
@@ -218,6 +234,7 @@ export const MemorySearchSchema = z
|
||||
.object({
|
||||
sessionMemory: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
|
||||
remote: z
|
||||
@@ -233,8 +250,10 @@ export const MemorySearchSchema = z
|
||||
pollIntervalMs: z.number().int().nonnegative().optional(),
|
||||
timeoutMinutes: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
fallback: z
|
||||
.union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
|
||||
@@ -245,6 +264,7 @@ export const MemorySearchSchema = z
|
||||
modelPath: z.string().optional(),
|
||||
modelCacheDir: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
store: z
|
||||
.object({
|
||||
@@ -255,14 +275,17 @@ export const MemorySearchSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
extensionPath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
chunking: z
|
||||
.object({
|
||||
tokens: z.number().int().positive().optional(),
|
||||
overlap: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
sync: z
|
||||
.object({
|
||||
@@ -272,6 +295,7 @@ export const MemorySearchSchema = z
|
||||
watchDebounceMs: z.number().int().nonnegative().optional(),
|
||||
intervalMinutes: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
query: z
|
||||
.object({
|
||||
@@ -284,25 +308,32 @@ export const MemorySearchSchema = z
|
||||
textWeight: z.number().min(0).max(1).optional(),
|
||||
candidateMultiplier: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
cache: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
maxEntries: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
export const AgentModelSchema = z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict(),
|
||||
]);
|
||||
export const AgentEntrySchema = z.object({
|
||||
export const AgentEntrySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
default: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
@@ -323,14 +354,16 @@ export const AgentEntrySchema = z.object({
|
||||
z.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
}),
|
||||
}).strict(),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
sandbox: AgentSandboxSchema,
|
||||
tools: AgentToolsSchema,
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const ToolsSchema = z
|
||||
.object({
|
||||
@@ -353,27 +386,33 @@ export const ToolsSchema = z
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
broadcast: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
agentToAgent: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
elevated: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: ElevatedAllowFromSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
exec: z
|
||||
.object({
|
||||
@@ -391,18 +430,23 @@ export const ToolsSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allowModels: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
subagents: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
tools: ToolPolicySchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -8,25 +8,31 @@ export const AgentsSchema = z
|
||||
defaults: z.lazy(() => AgentDefaultsSchema).optional(),
|
||||
list: z.array(AgentEntrySchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const BindingsSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
agentId: z.string(),
|
||||
match: z.object({
|
||||
channel: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
peer: z
|
||||
z
|
||||
.object({
|
||||
agentId: z.string(),
|
||||
match: z
|
||||
.object({
|
||||
kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]),
|
||||
id: z.string(),
|
||||
channel: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
peer: z
|
||||
.object({
|
||||
kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]),
|
||||
id: z.string(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
guildId: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
guildId: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
.strict(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional();
|
||||
|
||||
@@ -43,4 +49,5 @@ export const AudioSchema = z
|
||||
.object({
|
||||
transcription: TranscribeAudioSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -19,40 +19,48 @@ export const ModelCompatSchema = z
|
||||
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ModelDefinitionSchema = z.object({
|
||||
export const ModelDefinitionSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
api: ModelApiSchema.optional(),
|
||||
reasoning: z.boolean(),
|
||||
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
||||
cost: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cacheRead: z.number(),
|
||||
cacheWrite: z.number(),
|
||||
}),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cacheRead: z.number(),
|
||||
cacheWrite: z.number(),
|
||||
})
|
||||
.strict(),
|
||||
contextWindow: z.number().positive(),
|
||||
maxTokens: z.number().positive(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
compat: ModelCompatSchema,
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const ModelProviderSchema = z.object({
|
||||
export const ModelProviderSchema = z
|
||||
.object({
|
||||
baseUrl: z.string().min(1),
|
||||
apiKey: z.string().optional(),
|
||||
api: ModelApiSchema.optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
authHeader: z.boolean().optional(),
|
||||
models: z.array(ModelDefinitionSchema),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const ModelsConfigSchema = z
|
||||
.object({
|
||||
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
||||
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const GroupChatSchema = z
|
||||
@@ -60,11 +68,14 @@ export const GroupChatSchema = z
|
||||
mentionPatterns: z.array(z.string()).optional(),
|
||||
historyLimit: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const DmConfigSchema = z.object({
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
});
|
||||
export const DmConfigSchema = z
|
||||
.object({
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const IdentitySchema = z
|
||||
.object({
|
||||
@@ -72,6 +83,7 @@ export const IdentitySchema = z
|
||||
theme: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const QueueModeSchema = z.union([
|
||||
@@ -98,51 +110,59 @@ export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
||||
|
||||
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
||||
|
||||
export const BlockStreamingCoalesceSchema = z.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
idleMs: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export const BlockStreamingCoalesceSchema = z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
idleMs: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const BlockStreamingChunkSchema = 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(),
|
||||
});
|
||||
export const BlockStreamingChunkSchema = 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(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const HumanDelaySchema = z.object({
|
||||
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
||||
minMs: z.number().int().nonnegative().optional(),
|
||||
maxMs: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export const HumanDelaySchema = z
|
||||
.object({
|
||||
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
||||
minMs: z.number().int().nonnegative().optional(),
|
||||
maxMs: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const CliBackendSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
||||
maxPromptArgChars: z.number().int().positive().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
clearEnv: z.array(z.string()).optional(),
|
||||
modelArg: z.string().optional(),
|
||||
modelAliases: z.record(z.string(), z.string()).optional(),
|
||||
sessionArg: z.string().optional(),
|
||||
sessionArgs: z.array(z.string()).optional(),
|
||||
resumeArgs: z.array(z.string()).optional(),
|
||||
sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(),
|
||||
sessionIdFields: z.array(z.string()).optional(),
|
||||
systemPromptArg: z.string().optional(),
|
||||
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
||||
systemPromptWhen: z
|
||||
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
||||
.optional(),
|
||||
imageArg: z.string().optional(),
|
||||
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
|
||||
serialize: z.boolean().optional(),
|
||||
});
|
||||
export const CliBackendSchema = z
|
||||
.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
||||
maxPromptArgChars: z.number().int().positive().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
clearEnv: z.array(z.string()).optional(),
|
||||
modelArg: z.string().optional(),
|
||||
modelAliases: z.record(z.string(), z.string()).optional(),
|
||||
sessionArg: z.string().optional(),
|
||||
sessionArgs: z.array(z.string()).optional(),
|
||||
resumeArgs: z.array(z.string()).optional(),
|
||||
sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(),
|
||||
sessionIdFields: z.array(z.string()).optional(),
|
||||
systemPromptArg: z.string().optional(),
|
||||
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
||||
systemPromptWhen: z
|
||||
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
||||
.optional(),
|
||||
imageArg: z.string().optional(),
|
||||
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
|
||||
serialize: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
|
||||
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
@@ -173,6 +193,7 @@ export const RetryConfigSchema = z
|
||||
maxDelayMs: z.number().int().min(0).optional(),
|
||||
jitter: z.number().min(0).max(1).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const QueueModeBySurfaceSchema = z
|
||||
@@ -186,6 +207,7 @@ export const QueueModeBySurfaceSchema = z
|
||||
msteams: QueueModeSchema.optional(),
|
||||
webchat: QueueModeSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const DebounceMsBySurfaceSchema = z
|
||||
@@ -199,6 +221,7 @@ export const DebounceMsBySurfaceSchema = z
|
||||
msteams: z.number().int().nonnegative().optional(),
|
||||
webchat: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const QueueSchema = z
|
||||
@@ -209,6 +232,7 @@ export const QueueSchema = z
|
||||
cap: z.number().int().positive().optional(),
|
||||
drop: QueueDropSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const InboundDebounceSchema = z
|
||||
@@ -216,6 +240,7 @@ export const InboundDebounceSchema = z
|
||||
debounceMs: z.number().int().nonnegative().optional(),
|
||||
byChannel: DebounceMsBySurfaceSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const TranscribeAudioSchema = z
|
||||
@@ -232,6 +257,7 @@ export const TranscribeAudioSchema = z
|
||||
}),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
||||
@@ -245,21 +271,25 @@ export const MediaUnderstandingScopeSchema = z
|
||||
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
||||
rules: z
|
||||
.array(
|
||||
z.object({
|
||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||
match: z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||
match: z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const MediaUnderstandingCapabilitiesSchema = z
|
||||
@@ -274,6 +304,7 @@ export const MediaUnderstandingAttachmentsSchema = z
|
||||
.union([z.literal("first"), z.literal("last"), z.literal("path"), z.literal("url")])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const DeepgramAudioSchema = z
|
||||
@@ -282,6 +313,7 @@ const DeepgramAudioSchema = z
|
||||
punctuate: z.boolean().optional(),
|
||||
smartFormat: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const ProviderOptionValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
@@ -309,6 +341,7 @@ export const MediaUnderstandingModelSchema = z
|
||||
profile: z.string().optional(),
|
||||
preferredProfile: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsMediaUnderstandingSchema = z
|
||||
@@ -327,6 +360,7 @@ export const ToolsMediaUnderstandingSchema = z
|
||||
attachments: MediaUnderstandingAttachmentsSchema,
|
||||
models: z.array(MediaUnderstandingModelSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsMediaSchema = z
|
||||
@@ -337,6 +371,7 @@ export const ToolsMediaSchema = z
|
||||
audio: ToolsMediaUnderstandingSchema.optional(),
|
||||
video: ToolsMediaUnderstandingSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
||||
@@ -346,4 +381,5 @@ export const ProviderCommandsSchema = z
|
||||
native: NativeCommandsSettingSchema.optional(),
|
||||
nativeSkills: NativeCommandsSettingSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -37,22 +37,26 @@ export const HookMappingSchema = z
|
||||
module: z.string(),
|
||||
export: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const InternalHookHandlerSchema = z.object({
|
||||
event: z.string(),
|
||||
module: z.string(),
|
||||
export: z.string().optional(),
|
||||
});
|
||||
export const InternalHookHandlerSchema = z
|
||||
.object({
|
||||
event: z.string(),
|
||||
module: z.string(),
|
||||
export: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const HookConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
.strict();
|
||||
|
||||
const HookInstallRecordSchema = z
|
||||
.object({
|
||||
@@ -64,7 +68,7 @@ const HookInstallRecordSchema = z
|
||||
installedAt: z.string().optional(),
|
||||
hooks: z.array(z.string()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
.strict();
|
||||
|
||||
export const InternalHooksSchema = z
|
||||
.object({
|
||||
@@ -75,9 +79,11 @@ export const InternalHooksSchema = z
|
||||
.object({
|
||||
extraDirs: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
installs: z.record(z.string(), HookInstallRecordSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const HooksGmailSchema = z
|
||||
@@ -97,6 +103,7 @@ export const HooksGmailSchema = z
|
||||
port: z.number().int().positive().optional(),
|
||||
path: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
tailscale: z
|
||||
.object({
|
||||
@@ -104,6 +111,7 @@ export const HooksGmailSchema = z
|
||||
path: z.string().optional(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
thinking: z
|
||||
@@ -116,4 +124,5 @@ export const HooksGmailSchema = z
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -23,32 +23,40 @@ const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "a
|
||||
|
||||
const TelegramCapabilitiesSchema = z.union([
|
||||
z.array(z.string()),
|
||||
z.object({
|
||||
inlineButtons: TelegramInlineButtonsScopeSchema.optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
inlineButtons: TelegramInlineButtonsScopeSchema.optional(),
|
||||
})
|
||||
.strict(),
|
||||
]);
|
||||
|
||||
export const TelegramTopicSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
});
|
||||
export const TelegramTopicSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const TelegramGroupSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(),
|
||||
});
|
||||
export const TelegramGroupSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const TelegramCustomCommandSchema = z.object({
|
||||
command: z.string().transform(normalizeTelegramCommandName),
|
||||
description: z.string().transform(normalizeTelegramCommandDescription),
|
||||
});
|
||||
const TelegramCustomCommandSchema = z
|
||||
.object({
|
||||
command: z.string().transform(normalizeTelegramCommandName),
|
||||
description: z.string().transform(normalizeTelegramCommandDescription),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const validateTelegramCustomCommands = (
|
||||
value: { customCommands?: Array<{ command?: string; description?: string }> },
|
||||
@@ -69,7 +77,8 @@ const validateTelegramCustomCommands = (
|
||||
}
|
||||
};
|
||||
|
||||
export const TelegramAccountSchemaBase = z.object({
|
||||
export const TelegramAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: TelegramCapabilitiesSchema.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -105,10 +114,12 @@ export const TelegramAccountSchemaBase = z.object({
|
||||
sendMessage: z.boolean().optional(),
|
||||
deleteMessage: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
|
||||
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
@@ -144,6 +155,7 @@ export const DiscordDmSchema = z
|
||||
groupEnabled: z.boolean().optional(),
|
||||
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.policy,
|
||||
@@ -155,25 +167,30 @@ export const DiscordDmSchema = z
|
||||
});
|
||||
});
|
||||
|
||||
export const DiscordGuildChannelSchema = z.object({
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
autoThread: z.boolean().optional(),
|
||||
});
|
||||
export const DiscordGuildChannelSchema = z
|
||||
.object({
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
autoThread: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const DiscordGuildSchema = z.object({
|
||||
slug: z.string().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
||||
});
|
||||
export const DiscordGuildSchema = z
|
||||
.object({
|
||||
slug: z.string().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const DiscordAccountSchema = z.object({
|
||||
export const DiscordAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -212,11 +229,13 @@ export const DiscordAccountSchema = z.object({
|
||||
moderation: z.boolean().optional(),
|
||||
channels: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
replyToMode: ReplyToModeSchema.optional(),
|
||||
dm: DiscordDmSchema.optional(),
|
||||
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
||||
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
||||
@@ -230,6 +249,7 @@ export const SlackDmSchema = z
|
||||
groupEnabled: z.boolean().optional(),
|
||||
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.policy,
|
||||
@@ -241,22 +261,27 @@ export const SlackDmSchema = z
|
||||
});
|
||||
});
|
||||
|
||||
export const SlackChannelSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
allowBots: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
});
|
||||
export const SlackChannelSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
allowBots: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SlackThreadSchema = z.object({
|
||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||
inheritParent: z.boolean().optional(),
|
||||
});
|
||||
export const SlackThreadSchema = z
|
||||
.object({
|
||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||
inheritParent: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SlackAccountSchema = z.object({
|
||||
export const SlackAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
mode: z.enum(["socket", "http"]).optional(),
|
||||
signingSecret: z.string().optional(),
|
||||
@@ -294,6 +319,7 @@ export const SlackAccountSchema = z.object({
|
||||
channelInfo: z.boolean().optional(),
|
||||
emojiList: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
slashCommand: z
|
||||
.object({
|
||||
@@ -302,10 +328,12 @@ export const SlackAccountSchema = z.object({
|
||||
sessionPrefix: z.string().optional(),
|
||||
ephemeral: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
dm: SlackDmSchema.optional(),
|
||||
channels: z.record(z.string(), SlackChannelSchema.optional()).optional(),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SlackConfigSchema = SlackAccountSchema.extend({
|
||||
mode: z.enum(["socket", "http"]).optional().default("socket"),
|
||||
@@ -339,7 +367,8 @@ export const SlackConfigSchema = SlackAccountSchema.extend({
|
||||
}
|
||||
});
|
||||
|
||||
export const SignalAccountSchemaBase = z.object({
|
||||
export const SignalAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -367,7 +396,8 @@ export const SignalAccountSchemaBase = z.object({
|
||||
mediaMaxMb: z.number().int().positive().optional(),
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
@@ -391,7 +421,8 @@ export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
});
|
||||
});
|
||||
|
||||
export const IMessageAccountSchemaBase = z.object({
|
||||
export const IMessageAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -420,10 +451,12 @@ export const IMessageAccountSchemaBase = z.object({
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
@@ -449,16 +482,20 @@ export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
||||
});
|
||||
});
|
||||
|
||||
export const MSTeamsChannelSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
});
|
||||
export const MSTeamsChannelSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const MSTeamsTeamSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
||||
});
|
||||
export const MSTeamsTeamSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const MSTeamsConfigSchema = z
|
||||
.object({
|
||||
@@ -473,6 +510,7 @@ export const MSTeamsConfigSchema = z
|
||||
port: z.number().int().positive().optional(),
|
||||
path: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
@@ -488,6 +526,7 @@ export const MSTeamsConfigSchema = z
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
|
||||
@@ -36,6 +36,7 @@ export const WhatsAppAccountSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
@@ -45,9 +46,11 @@ export const WhatsAppAccountSchema = z
|
||||
direct: z.boolean().optional().default(true),
|
||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
debounceMs: z.number().int().nonnegative().optional().default(0),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.dmPolicy !== "open") return;
|
||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
@@ -84,6 +87,7 @@ export const WhatsAppConfigSchema = z
|
||||
sendMessage: z.boolean().optional(),
|
||||
polls: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
groups: z
|
||||
.record(
|
||||
@@ -92,6 +96,7 @@ export const WhatsAppConfigSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
@@ -101,9 +106,11 @@ export const WhatsAppConfigSchema = z
|
||||
direct: z.boolean().optional().default(true),
|
||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
debounceMs: z.number().int().nonnegative().optional().default(0),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.dmPolicy !== "open") return;
|
||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
|
||||
@@ -20,6 +20,7 @@ export const ChannelsSchema = z
|
||||
.object({
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
whatsapp: WhatsAppConfigSchema.optional(),
|
||||
telegram: TelegramConfigSchema.optional(),
|
||||
@@ -29,5 +30,5 @@ export const ChannelsSchema = z
|
||||
imessage: IMessageConfigSchema.optional(),
|
||||
msteams: MSTeamsConfigSchema.optional(),
|
||||
})
|
||||
.catchall(z.unknown())
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
QueueSchema,
|
||||
} from "./zod-schema.core.js";
|
||||
|
||||
const SessionResetConfigSchema = z.object({
|
||||
mode: z.union([z.literal("daily"), z.literal("idle")]).optional(),
|
||||
atHour: z.number().int().min(0).max(23).optional(),
|
||||
idleMinutes: z.number().int().positive().optional(),
|
||||
});
|
||||
const SessionResetConfigSchema = z
|
||||
.object({
|
||||
mode: z.union([z.literal("daily"), z.literal("idle")]).optional(),
|
||||
atHour: z.number().int().min(0).max(23).optional(),
|
||||
idleMinutes: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SessionSchema = z
|
||||
.object({
|
||||
@@ -30,6 +32,7 @@ export const SessionSchema = z
|
||||
group: SessionResetConfigSchema.optional(),
|
||||
thread: SessionResetConfigSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
store: z.string().optional(),
|
||||
typingIntervalSeconds: z.number().int().positive().optional(),
|
||||
@@ -47,28 +50,34 @@ export const SessionSchema = z
|
||||
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
||||
rules: z
|
||||
.array(
|
||||
z.object({
|
||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||
match: z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||
match: z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
agentToAgent: z
|
||||
.object({
|
||||
maxPingPongTurns: z.number().int().min(0).max(5).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const MessagesSchema = z
|
||||
@@ -82,6 +91,7 @@ export const MessagesSchema = z
|
||||
ackReactionScope: z.enum(["group-mentions", "group-all", "direct", "all"]).optional(),
|
||||
removeAckAfterReply: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const CommandsSchema = z
|
||||
@@ -96,5 +106,6 @@ export const CommandsSchema = z
|
||||
restart: z.boolean().optional(),
|
||||
useAccessGroups: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional()
|
||||
.default({ native: "auto", nativeSkills: "auto" });
|
||||
|
||||
@@ -13,6 +13,7 @@ export const ClawdbotSchema = z
|
||||
lastTouchedVersion: z.string().optional(),
|
||||
lastTouchedAt: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
env: z
|
||||
.object({
|
||||
@@ -21,6 +22,7 @@ export const ClawdbotSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
timeoutMs: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
vars: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
@@ -34,6 +36,7 @@ export const ClawdbotSchema = z
|
||||
lastRunCommand: z.string().optional(),
|
||||
lastRunMode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
logging: z
|
||||
.object({
|
||||
@@ -66,12 +69,14 @@ export const ClawdbotSchema = z
|
||||
redactSensitive: z.union([z.literal("off"), z.literal("tools")]).optional(),
|
||||
redactPatterns: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
update: z
|
||||
.object({
|
||||
channel: z.union([z.literal("stable"), z.literal("beta")]).optional(),
|
||||
checkOnStart: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
browser: z
|
||||
.object({
|
||||
@@ -99,28 +104,33 @@ export const ClawdbotSchema = z
|
||||
driver: z.union([z.literal("clawd"), z.literal("extension")]).optional(),
|
||||
color: HexColorSchema,
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => value.cdpPort || value.cdpUrl, {
|
||||
message: "Profile must set cdpPort or cdpUrl",
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
ui: z
|
||||
.object({
|
||||
seamColor: HexColorSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
auth: z
|
||||
.object({
|
||||
profiles: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
|
||||
email: z.string().optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
provider: z.string(),
|
||||
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
|
||||
email: z.string().optional(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
order: z.record(z.string(), z.array(z.string())).optional(),
|
||||
@@ -131,8 +141,10 @@ export const ClawdbotSchema = z
|
||||
billingMaxHours: z.number().positive().optional(),
|
||||
failureWindowHours: z.number().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
models: ModelsConfigSchema,
|
||||
agents: AgentsSchema,
|
||||
@@ -149,6 +161,7 @@ export const ClawdbotSchema = z
|
||||
store: z.string().optional(),
|
||||
maxConcurrentRuns: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
hooks: z
|
||||
.object({
|
||||
@@ -162,6 +175,7 @@ export const ClawdbotSchema = z
|
||||
gmail: HooksGmailSchema,
|
||||
internal: InternalHooksSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
web: z
|
||||
.object({
|
||||
@@ -175,8 +189,10 @@ export const ClawdbotSchema = z
|
||||
jitter: z.number().min(0).max(1).optional(),
|
||||
maxAttempts: z.number().int().min(0).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
channels: ChannelsSchema,
|
||||
bridge: z
|
||||
@@ -194,8 +210,10 @@ export const ClawdbotSchema = z
|
||||
keyPath: z.string().optional(),
|
||||
caPath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
discovery: z
|
||||
.object({
|
||||
@@ -203,8 +221,10 @@ export const ClawdbotSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
canvasHost: z
|
||||
.object({
|
||||
@@ -213,6 +233,7 @@ export const ClawdbotSchema = z
|
||||
port: z.number().int().positive().optional(),
|
||||
liveReload: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
talk: z
|
||||
.object({
|
||||
@@ -223,6 +244,7 @@ export const ClawdbotSchema = z
|
||||
apiKey: z.string().optional(),
|
||||
interruptOnSpeech: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
gateway: z
|
||||
.object({
|
||||
@@ -236,6 +258,7 @@ export const ClawdbotSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
basePath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
auth: z
|
||||
.object({
|
||||
@@ -244,12 +267,14 @@ export const ClawdbotSchema = z
|
||||
password: z.string().optional(),
|
||||
allowTailscale: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
tailscale: z
|
||||
.object({
|
||||
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
|
||||
resetOnExit: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
remote: z
|
||||
.object({
|
||||
@@ -259,6 +284,7 @@ export const ClawdbotSchema = z
|
||||
sshTarget: z.string().optional(),
|
||||
sshIdentity: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
reload: z
|
||||
.object({
|
||||
@@ -272,6 +298,7 @@ export const ClawdbotSchema = z
|
||||
.optional(),
|
||||
debounceMs: z.number().int().min(0).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
http: z
|
||||
.object({
|
||||
@@ -281,12 +308,16 @@ export const ClawdbotSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
skills: z
|
||||
.object({
|
||||
@@ -297,6 +328,7 @@ export const ClawdbotSchema = z
|
||||
watch: z.boolean().optional(),
|
||||
watchDebounceMs: z.number().int().min(0).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
install: z
|
||||
.object({
|
||||
@@ -305,6 +337,7 @@ export const ClawdbotSchema = z
|
||||
.union([z.literal("npm"), z.literal("pnpm"), z.literal("yarn"), z.literal("bun")])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
entries: z
|
||||
.record(
|
||||
@@ -315,10 +348,11 @@ export const ClawdbotSchema = z
|
||||
apiKey: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
plugins: z
|
||||
.object({
|
||||
@@ -329,11 +363,13 @@ export const ClawdbotSchema = z
|
||||
.object({
|
||||
paths: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
slots: z
|
||||
.object({
|
||||
memory: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
entries: z
|
||||
.record(
|
||||
@@ -343,7 +379,7 @@ export const ClawdbotSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
installs: z
|
||||
@@ -358,13 +394,14 @@ export const ClawdbotSchema = z
|
||||
version: z.string().optional(),
|
||||
installedAt: z.string().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.strict()
|
||||
.superRefine((cfg, ctx) => {
|
||||
const agents = cfg.agents?.list ?? [];
|
||||
if (agents.length === 0) return;
|
||||
|
||||
Reference in New Issue
Block a user