mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor: dedupe core config and runtime helpers
This commit is contained in:
@@ -1,107 +1,80 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import { withTempHomeConfig } from "./test-helpers.js";
|
||||
|
||||
describe("config compaction settings", () => {
|
||||
it("preserves memory flush config values", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
mode: "safeguard",
|
||||
reserveTokensFloor: 12_345,
|
||||
memoryFlush: {
|
||||
enabled: false,
|
||||
softThresholdTokens: 1234,
|
||||
prompt: "Write notes.",
|
||||
systemPrompt: "Flush memory now.",
|
||||
},
|
||||
},
|
||||
await withTempHomeConfig(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
mode: "safeguard",
|
||||
reserveTokensFloor: 12_345,
|
||||
memoryFlush: {
|
||||
enabled: false,
|
||||
softThresholdTokens: 1234,
|
||||
prompt: "Write notes.",
|
||||
systemPrompt: "Flush memory now.",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(12_345);
|
||||
expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard");
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBeUndefined();
|
||||
expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBeUndefined();
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(false);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens).toBe(1234);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe("Write notes.");
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.systemPrompt).toBe("Flush memory now.");
|
||||
});
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(12_345);
|
||||
expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard");
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBeUndefined();
|
||||
expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBeUndefined();
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(false);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens).toBe(1234);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe("Write notes.");
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.systemPrompt).toBe(
|
||||
"Flush memory now.",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves pi compaction override values", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
reserveTokens: 15_000,
|
||||
keepRecentTokens: 12_000,
|
||||
},
|
||||
},
|
||||
await withTempHomeConfig(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
reserveTokens: 15_000,
|
||||
keepRecentTokens: 12_000,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBe(15_000);
|
||||
expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBe(12_000);
|
||||
});
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBe(15_000);
|
||||
expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBe(12_000);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults compaction mode to safeguard", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
reserveTokensFloor: 9000,
|
||||
},
|
||||
},
|
||||
await withTempHomeConfig(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
reserveTokensFloor: 9000,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard");
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(9000);
|
||||
});
|
||||
expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard");
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(9000);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { loadConfig, validateConfigObject } from "./config.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import { withTempHomeConfig } from "./test-helpers.js";
|
||||
|
||||
describe("config discord", () => {
|
||||
let previousHome: string | undefined;
|
||||
@@ -16,57 +14,48 @@ describe("config discord", () => {
|
||||
});
|
||||
|
||||
it("loads discord guild map + dm group settings", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
dm: {
|
||||
enabled: true,
|
||||
allowFrom: ["steipete"],
|
||||
groupEnabled: true,
|
||||
groupChannels: ["openclaw-dm"],
|
||||
},
|
||||
actions: {
|
||||
emojiUploads: true,
|
||||
stickerUploads: false,
|
||||
channels: true,
|
||||
},
|
||||
guilds: {
|
||||
"123": {
|
||||
slug: "friends-of-openclaw",
|
||||
requireMention: false,
|
||||
users: ["steipete"],
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
await withTempHomeConfig(
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
dm: {
|
||||
enabled: true,
|
||||
allowFrom: ["steipete"],
|
||||
groupEnabled: true,
|
||||
groupChannels: ["openclaw-dm"],
|
||||
},
|
||||
actions: {
|
||||
emojiUploads: true,
|
||||
stickerUploads: false,
|
||||
channels: true,
|
||||
},
|
||||
guilds: {
|
||||
"123": {
|
||||
slug: "friends-of-openclaw",
|
||||
requireMention: false,
|
||||
users: ["steipete"],
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.channels?.discord?.enabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupEnabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupChannels).toEqual(["openclaw-dm"]);
|
||||
expect(cfg.channels?.discord?.actions?.emojiUploads).toBe(true);
|
||||
expect(cfg.channels?.discord?.actions?.stickerUploads).toBe(false);
|
||||
expect(cfg.channels?.discord?.actions?.channels).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe("friends-of-openclaw");
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true);
|
||||
});
|
||||
expect(cfg.channels?.discord?.enabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupEnabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupChannels).toEqual(["openclaw-dm"]);
|
||||
expect(cfg.channels?.discord?.actions?.emojiUploads).toBe(true);
|
||||
expect(cfg.channels?.discord?.actions?.stickerUploads).toBe(false);
|
||||
expect(cfg.channels?.discord?.actions?.channels).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe("friends-of-openclaw");
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects numeric discord allowlist entries", () => {
|
||||
|
||||
@@ -26,6 +26,22 @@ async function expectLoadRejectionPreservesField(params: {
|
||||
});
|
||||
}
|
||||
|
||||
type ConfigSnapshot = Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
|
||||
async function withSnapshotForConfig(
|
||||
config: unknown,
|
||||
run: (params: { snapshot: ConfigSnapshot; parsed: unknown; configPath: string }) => Promise<void>,
|
||||
) {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
|
||||
await run({ snapshot, parsed, configPath });
|
||||
});
|
||||
}
|
||||
|
||||
function expectValidConfigValue(params: {
|
||||
config: unknown;
|
||||
readValue: (config: unknown) => unknown;
|
||||
@@ -47,6 +63,20 @@ function expectInvalidIssuePath(config: unknown, expectedPath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function expectRoutingAllowFromLegacySnapshot(
|
||||
ctx: { snapshot: ConfigSnapshot; parsed: unknown },
|
||||
expectedAllowFrom: string[],
|
||||
) {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||
const parsed = ctx.parsed as {
|
||||
routing?: { allowFrom?: string[] };
|
||||
channels?: unknown;
|
||||
};
|
||||
expect(parsed.routing?.allowFrom).toEqual(expectedAllowFrom);
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
}
|
||||
|
||||
describe("legacy config detection", () => {
|
||||
it('accepts imessage.dmPolicy="open" with allowFrom "*"', async () => {
|
||||
const res = validateConfigObject({
|
||||
@@ -224,43 +254,30 @@ describe("legacy config detection", () => {
|
||||
expect((res.config as { agent?: unknown } | undefined)?.agent).toBeUndefined();
|
||||
});
|
||||
it("flags legacy config in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ routing: { allowFrom: ["+15555550123"] } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
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 {
|
||||
routing?: { allowFrom?: string[] };
|
||||
channels?: unknown;
|
||||
};
|
||||
expect(parsed.routing?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
await withSnapshotForConfig({ routing: { allowFrom: ["+15555550123"] } }, async (ctx) => {
|
||||
expectRoutingAllowFromLegacySnapshot(ctx, ["+15555550123"]);
|
||||
});
|
||||
});
|
||||
it("flags top-level memorySearch as legacy in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ memorySearch: { provider: "local", fallback: "none" } }),
|
||||
"utf-8",
|
||||
);
|
||||
await withSnapshotForConfig(
|
||||
{ memorySearch: { provider: "local", fallback: "none" } },
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
it("flags legacy provider sections in snapshot", async () => {
|
||||
await withSnapshotForConfig({ whatsapp: { allowFrom: ["+1555"] } }, async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
|
||||
const parsed = ctx.parsed as {
|
||||
channels?: unknown;
|
||||
whatsapp?: unknown;
|
||||
};
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
expect(parsed.whatsapp).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
|
||||
@@ -293,52 +310,9 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||
});
|
||||
});
|
||||
it("flags legacy provider sections in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ whatsapp: { allowFrom: ["+1555"] } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
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("flags routing.allowFrom in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ routing: { allowFrom: ["+1666"] } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
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"]);
|
||||
await withSnapshotForConfig({ routing: { allowFrom: ["+1666"] } }, async (ctx) => {
|
||||
expectRoutingAllowFromLegacySnapshot(ctx, ["+1666"]);
|
||||
});
|
||||
});
|
||||
it("rejects bindings[].match.provider on load", async () => {
|
||||
@@ -374,61 +348,40 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
});
|
||||
it("rejects session.sendPolicy.rules[].match.provider on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
session: {
|
||||
sendPolicy: {
|
||||
rules: [{ action: "deny", match: { provider: "telegram" } }],
|
||||
},
|
||||
},
|
||||
await withSnapshotForConfig(
|
||||
{
|
||||
session: {
|
||||
sendPolicy: {
|
||||
rules: [{ action: "deny", match: { provider: "telegram" } }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
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");
|
||||
});
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.issues.length).toBeGreaterThan(0);
|
||||
const parsed = ctx.parsed as {
|
||||
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
|
||||
};
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBe("telegram");
|
||||
},
|
||||
);
|
||||
});
|
||||
it("rejects messages.queue.byProvider on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ messages: { queue: { byProvider: { whatsapp: "queue" } } } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
await withSnapshotForConfig(
|
||||
{ messages: { queue: { byProvider: { whatsapp: "queue" } } } },
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
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>;
|
||||
const parsed = ctx.parsed as {
|
||||
messages?: {
|
||||
queue?: {
|
||||
byProvider?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
|
||||
});
|
||||
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadConfig, validateConfigObject } from "./config.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import { withTempHomeConfig } from "./test-helpers.js";
|
||||
|
||||
describe("multi-agent agentDir validation", () => {
|
||||
it("rejects shared agents.list agentDir", async () => {
|
||||
@@ -24,31 +23,22 @@ describe("multi-agent agentDir validation", () => {
|
||||
});
|
||||
|
||||
it("throws on shared agentDir during loadConfig()", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "a", agentDir: "~/.openclaw/agents/shared/agent" },
|
||||
{ id: "b", agentDir: "~/.openclaw/agents/shared/agent" },
|
||||
],
|
||||
},
|
||||
bindings: [{ agentId: "a", match: { channel: "telegram" } }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
expect(() => loadConfig()).toThrow(/duplicate agentDir/i);
|
||||
expect(spy.mock.calls.flat().join(" ")).toMatch(/Duplicate agentDir/i);
|
||||
spy.mockRestore();
|
||||
});
|
||||
await withTempHomeConfig(
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "a", agentDir: "~/.openclaw/agents/shared/agent" },
|
||||
{ id: "b", agentDir: "~/.openclaw/agents/shared/agent" },
|
||||
],
|
||||
},
|
||||
bindings: [{ agentId: "a", match: { channel: "telegram" } }],
|
||||
},
|
||||
async () => {
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
expect(() => loadConfig()).toThrow(/duplicate agentDir/i);
|
||||
expect(spy.mock.calls.flat().join(" ")).toMatch(/Duplicate agentDir/i);
|
||||
spy.mockRestore();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
resolveIsNixMode,
|
||||
resolveStateDir,
|
||||
} from "./config.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import { withTempHome, withTempHomeConfig } from "./test-helpers.js";
|
||||
|
||||
function envWith(overrides: Record<string, string | undefined>): NodeJS.ProcessEnv {
|
||||
// Hermetic env: don't inherit process.env because other tests may mutate it.
|
||||
@@ -23,6 +23,16 @@ function loadConfigForHome(home: string) {
|
||||
}).loadConfig();
|
||||
}
|
||||
|
||||
async function withLoadedConfigForHome(
|
||||
config: unknown,
|
||||
run: (cfg: ReturnType<typeof loadConfigForHome>) => Promise<void> | void,
|
||||
) {
|
||||
await withTempHomeConfig(config, async ({ home }) => {
|
||||
const cfg = loadConfigForHome(home);
|
||||
await run(cfg);
|
||||
});
|
||||
}
|
||||
|
||||
describe("Nix integration (U3, U5, U9)", () => {
|
||||
describe("U3: isNixMode env var detection", () => {
|
||||
it("isNixMode is false when OPENCLAW_NIX_MODE is not set", () => {
|
||||
@@ -211,62 +221,44 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
|
||||
describe("U9: telegram.tokenFile schema validation", () => {
|
||||
it("accepts config with only botToken", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify({
|
||||
channels: { telegram: { botToken: "123:ABC" } },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg = loadConfigForHome(home);
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("123:ABC");
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBeUndefined();
|
||||
});
|
||||
await withLoadedConfigForHome(
|
||||
{
|
||||
channels: { telegram: { botToken: "123:ABC" } },
|
||||
},
|
||||
async (cfg) => {
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("123:ABC");
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts config with only tokenFile", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify({
|
||||
channels: { telegram: { tokenFile: "/run/agenix/telegram-token" } },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg = loadConfigForHome(home);
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
|
||||
});
|
||||
await withLoadedConfigForHome(
|
||||
{
|
||||
channels: { telegram: { tokenFile: "/run/agenix/telegram-token" } },
|
||||
},
|
||||
async (cfg) => {
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts config with both botToken and tokenFile", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify({
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "fallback:token",
|
||||
tokenFile: "/run/agenix/telegram-token",
|
||||
},
|
||||
await withLoadedConfigForHome(
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "fallback:token",
|
||||
tokenFile: "/run/agenix/telegram-token",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg = loadConfigForHome(home);
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("fallback:token");
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
});
|
||||
},
|
||||
},
|
||||
async (cfg) => {
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("fallback:token");
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,28 @@ async function withWrapperEnvContext(configPath: string, run: () => Promise<void
|
||||
);
|
||||
}
|
||||
|
||||
function createGatewayTokenConfigJson(): string {
|
||||
return JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
||||
}
|
||||
|
||||
function createMutableApiKeyEnv(initialValue = "original-key-123"): Record<string, string> {
|
||||
return { MY_API_KEY: initialValue };
|
||||
}
|
||||
|
||||
async function withGatewayTokenTempConfig(
|
||||
run: (configPath: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
await withTempConfig(createGatewayTokenConfigJson(), run);
|
||||
}
|
||||
|
||||
async function withWrapperGatewayTokenContext(
|
||||
run: (configPath: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
await withGatewayTokenTempConfig(async (configPath) => {
|
||||
await withWrapperEnvContext(configPath, async () => run(configPath));
|
||||
});
|
||||
}
|
||||
|
||||
async function readGatewayToken(configPath: string): Promise<string> {
|
||||
const written = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(written) as { gateway: { remote: { token: string } } };
|
||||
@@ -70,13 +92,8 @@ async function readGatewayToken(configPath: string): Promise<string> {
|
||||
|
||||
describe("env snapshot TOCTOU via createConfigIO", () => {
|
||||
it("restores env refs using read-time env even after env mutation", async () => {
|
||||
const env: Record<string, string> = {
|
||||
MY_API_KEY: "original-key-123",
|
||||
};
|
||||
|
||||
const configJson = JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
||||
|
||||
await withTempConfig(configJson, async (configPath) => {
|
||||
const env = createMutableApiKeyEnv();
|
||||
await withGatewayTokenTempConfig(async (configPath) => {
|
||||
// Instance A: read config (captures env snapshot)
|
||||
const ioA = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
||||
const firstRead = await ioA.readConfigFileSnapshotForWrite();
|
||||
@@ -99,13 +116,8 @@ describe("env snapshot TOCTOU via createConfigIO", () => {
|
||||
});
|
||||
|
||||
it("without snapshot bridging, mutated env causes incorrect restoration", async () => {
|
||||
const env: Record<string, string> = {
|
||||
MY_API_KEY: "original-key-123",
|
||||
};
|
||||
|
||||
const configJson = JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
||||
|
||||
await withTempConfig(configJson, async (configPath) => {
|
||||
const env = createMutableApiKeyEnv();
|
||||
await withGatewayTokenTempConfig(async (configPath) => {
|
||||
// Instance A: read config
|
||||
const ioA = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
||||
const snapshot = await ioA.readConfigFileSnapshot();
|
||||
@@ -132,40 +144,34 @@ describe("env snapshot TOCTOU via createConfigIO", () => {
|
||||
|
||||
describe("env snapshot TOCTOU via wrapper APIs", () => {
|
||||
it("uses explicit read context even if another read interleaves", async () => {
|
||||
const configJson = JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
||||
await withTempConfig(configJson, async (configPath) => {
|
||||
await withWrapperEnvContext(configPath, async () => {
|
||||
const firstRead = await readConfigFileSnapshotForWrite();
|
||||
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
||||
await withWrapperGatewayTokenContext(async (configPath) => {
|
||||
const firstRead = await readConfigFileSnapshotForWrite();
|
||||
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
||||
|
||||
// Interleaving read from another request context with a different env value.
|
||||
process.env.MY_API_KEY = "mutated-key-456";
|
||||
const secondRead = await readConfigFileSnapshotForWrite();
|
||||
expect(secondRead.snapshot.config.gateway?.remote?.token).toBe("mutated-key-456");
|
||||
// Interleaving read from another request context with a different env value.
|
||||
process.env.MY_API_KEY = "mutated-key-456";
|
||||
const secondRead = await readConfigFileSnapshotForWrite();
|
||||
expect(secondRead.snapshot.config.gateway?.remote?.token).toBe("mutated-key-456");
|
||||
|
||||
// Write using the first read's explicit context.
|
||||
await writeConfigFileViaWrapper(firstRead.snapshot.config, firstRead.writeOptions);
|
||||
expect(await readGatewayToken(configPath)).toBe("${MY_API_KEY}");
|
||||
});
|
||||
// Write using the first read's explicit context.
|
||||
await writeConfigFileViaWrapper(firstRead.snapshot.config, firstRead.writeOptions);
|
||||
expect(await readGatewayToken(configPath)).toBe("${MY_API_KEY}");
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores read context when expected config path does not match", async () => {
|
||||
const configJson = JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
||||
await withTempConfig(configJson, async (configPath) => {
|
||||
await withWrapperEnvContext(configPath, async () => {
|
||||
const firstRead = await readConfigFileSnapshotForWrite();
|
||||
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
||||
expect(firstRead.writeOptions.expectedConfigPath).toBe(configPath);
|
||||
await withWrapperGatewayTokenContext(async (configPath) => {
|
||||
const firstRead = await readConfigFileSnapshotForWrite();
|
||||
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
||||
expect(firstRead.writeOptions.expectedConfigPath).toBe(configPath);
|
||||
|
||||
process.env.MY_API_KEY = "mutated-key-456";
|
||||
await writeConfigFileViaWrapper(firstRead.snapshot.config, {
|
||||
...firstRead.writeOptions,
|
||||
expectedConfigPath: `${configPath}.different`,
|
||||
});
|
||||
|
||||
expect(await readGatewayToken(configPath)).toBe("original-key-123");
|
||||
process.env.MY_API_KEY = "mutated-key-456";
|
||||
await writeConfigFileViaWrapper(firstRead.snapshot.config, {
|
||||
...firstRead.writeOptions,
|
||||
expectedConfigPath: `${configPath}.different`,
|
||||
});
|
||||
|
||||
expect(await readGatewayToken(configPath)).toBe("original-key-123");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,27 @@ function restoreRedactedValues<TOriginal>(
|
||||
return result.result as TOriginal;
|
||||
}
|
||||
|
||||
function expectNestedLevelPairValue(
|
||||
source: Record<string, Record<string, Record<string, unknown>>>,
|
||||
field: string,
|
||||
expected: readonly [unknown, unknown],
|
||||
): void {
|
||||
const values = source.nested.level[field] as unknown[];
|
||||
expect(values[0]).toBe(expected[0]);
|
||||
expect(values[1]).toBe(expected[1]);
|
||||
}
|
||||
|
||||
function expectGatewayAuthFieldValue(
|
||||
result: ReturnType<typeof redactConfigSnapshot>,
|
||||
field: "token" | "password",
|
||||
expected: string,
|
||||
): void {
|
||||
const gateway = result.config.gateway as Record<string, Record<string, string>>;
|
||||
const resolved = result.resolved as Record<string, Record<string, Record<string, string>>>;
|
||||
expect(gateway.auth[field]).toBe(expected);
|
||||
expect(resolved.gateway.auth[field]).toBe(expected);
|
||||
}
|
||||
|
||||
describe("redactConfigSnapshot", () => {
|
||||
it("redacts common secret field patterns across config sections", () => {
|
||||
const snapshot = makeSnapshot({
|
||||
@@ -560,12 +581,10 @@ describe("redactConfigSnapshot", () => {
|
||||
}),
|
||||
assert: ({ redacted, restored }) => {
|
||||
const cfg = redacted as Record<string, Record<string, Record<string, unknown>>>;
|
||||
expect((cfg.nested.level.token as unknown[])[0]).toBe(42);
|
||||
expect((cfg.nested.level.token as unknown[])[1]).toBe(815);
|
||||
expectNestedLevelPairValue(cfg, "token", [42, 815]);
|
||||
|
||||
const out = restored as Record<string, Record<string, Record<string, unknown>>>;
|
||||
expect((out.nested.level.token as unknown[])[0]).toBe(42);
|
||||
expect((out.nested.level.token as unknown[])[1]).toBe(815);
|
||||
expectNestedLevelPairValue(out, "token", [42, 815]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -604,12 +623,10 @@ describe("redactConfigSnapshot", () => {
|
||||
}),
|
||||
assert: ({ redacted, restored }) => {
|
||||
const cfg = redacted as Record<string, Record<string, Record<string, unknown>>>;
|
||||
expect((cfg.nested.level.custom as unknown[])[0]).toBe(42);
|
||||
expect((cfg.nested.level.custom as unknown[])[1]).toBe(815);
|
||||
expectNestedLevelPairValue(cfg, "custom", [42, 815]);
|
||||
|
||||
const out = restored as Record<string, Record<string, Record<string, unknown>>>;
|
||||
expect((out.nested.level.custom as unknown[])[0]).toBe(42);
|
||||
expect((out.nested.level.custom as unknown[])[1]).toBe(815);
|
||||
expectNestedLevelPairValue(out, "custom", [42, 815]);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -636,10 +653,7 @@ describe("redactConfigSnapshot", () => {
|
||||
gateway: { auth: { token: "not-actually-secret-value" } },
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
const gw = result.config.gateway as Record<string, Record<string, string>>;
|
||||
const resolved = result.resolved as Record<string, Record<string, Record<string, string>>>;
|
||||
expect(gw.auth.token).toBe("not-actually-secret-value");
|
||||
expect(resolved.gateway.auth.token).toBe("not-actually-secret-value");
|
||||
expectGatewayAuthFieldValue(result, "token", "not-actually-secret-value");
|
||||
});
|
||||
|
||||
it("does not redact paths absent from uiHints (schema is single source of truth)", () => {
|
||||
@@ -650,10 +664,7 @@ describe("redactConfigSnapshot", () => {
|
||||
gateway: { auth: { password: "not-in-hints-value" } },
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
const gw = result.config.gateway as Record<string, Record<string, string>>;
|
||||
const resolved = result.resolved as Record<string, Record<string, Record<string, string>>>;
|
||||
expect(gw.auth.password).toBe("not-in-hints-value");
|
||||
expect(resolved.gateway.auth.password).toBe("not-in-hints-value");
|
||||
expectGatewayAuthFieldValue(result, "password", "not-in-hints-value");
|
||||
});
|
||||
|
||||
it("uses wildcard hints for array items", () => {
|
||||
|
||||
@@ -43,6 +43,13 @@ async function createCaseDir(prefix: string): Promise<string> {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createStaleAndFreshStore(now = Date.now()): Record<string, SessionEntry> {
|
||||
return {
|
||||
stale: makeEntry(now - 30 * DAY_MS),
|
||||
fresh: makeEntry(now),
|
||||
};
|
||||
}
|
||||
|
||||
describe("Integration: saveSessionStore with pruning", () => {
|
||||
let testDir: string;
|
||||
let storePath: string;
|
||||
@@ -78,11 +85,7 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
it("saveSessionStore prunes stale entries on write", async () => {
|
||||
applyEnforcedMaintenanceConfig(mockLoadConfig);
|
||||
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
stale: makeEntry(now - 30 * DAY_MS),
|
||||
fresh: makeEntry(now),
|
||||
};
|
||||
const store = createStaleAndFreshStore();
|
||||
|
||||
await saveSessionStore(storePath, store);
|
||||
|
||||
@@ -168,11 +171,7 @@ describe("Integration: saveSessionStore with pruning", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
stale: makeEntry(now - 30 * DAY_MS),
|
||||
fresh: makeEntry(now),
|
||||
};
|
||||
const store = createStaleAndFreshStore();
|
||||
|
||||
await saveSessionStore(storePath, store);
|
||||
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
|
||||
export async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
return withTempHomeBase(fn, { prefix: "openclaw-config-" });
|
||||
}
|
||||
|
||||
export async function writeOpenClawConfig(home: string, config: unknown): Promise<string> {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
return configPath;
|
||||
}
|
||||
|
||||
export async function withTempHomeConfig<T>(
|
||||
config: unknown,
|
||||
fn: (params: { home: string; configPath: string }) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return withTempHome(async (home) => {
|
||||
const configPath = await writeOpenClawConfig(home, config);
|
||||
return fn({ home, configPath });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to test env var overrides. Saves/restores env vars for a callback.
|
||||
*/
|
||||
|
||||
@@ -440,13 +440,17 @@ export const AgentSandboxSchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const CommonToolPolicyFields = {
|
||||
profile: ToolProfileSchema,
|
||||
allow: z.array(z.string()).optional(),
|
||||
alsoAllow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
||||
};
|
||||
|
||||
export const AgentToolsSchema = z
|
||||
.object({
|
||||
profile: ToolProfileSchema,
|
||||
allow: z.array(z.string()).optional(),
|
||||
alsoAllow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
||||
...CommonToolPolicyFields,
|
||||
elevated: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -641,11 +645,7 @@ export const AgentEntrySchema = z
|
||||
|
||||
export const ToolsSchema = z
|
||||
.object({
|
||||
profile: ToolProfileSchema,
|
||||
allow: z.array(z.string()).optional(),
|
||||
alsoAllow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
||||
...CommonToolPolicyFields,
|
||||
web: ToolsWebSchema,
|
||||
media: ToolsMediaSchema,
|
||||
links: ToolsLinksSchema,
|
||||
|
||||
@@ -430,6 +430,16 @@ const ProviderOptionsSchema = z
|
||||
.record(z.string(), z.record(z.string(), ProviderOptionValueSchema))
|
||||
.optional();
|
||||
|
||||
const MediaUnderstandingRuntimeFields = {
|
||||
prompt: z.string().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
language: z.string().optional(),
|
||||
providerOptions: ProviderOptionsSchema,
|
||||
deepgram: DeepgramAudioSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
};
|
||||
|
||||
export const MediaUnderstandingModelSchema = z
|
||||
.object({
|
||||
provider: z.string().optional(),
|
||||
@@ -438,15 +448,9 @@ export const MediaUnderstandingModelSchema = z
|
||||
type: z.union([z.literal("provider"), z.literal("cli")]).optional(),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
prompt: z.string().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
maxBytes: z.number().int().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
language: z.string().optional(),
|
||||
providerOptions: ProviderOptionsSchema,
|
||||
deepgram: DeepgramAudioSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
...MediaUnderstandingRuntimeFields,
|
||||
profile: z.string().optional(),
|
||||
preferredProfile: z.string().optional(),
|
||||
})
|
||||
@@ -459,13 +463,7 @@ export const ToolsMediaUnderstandingSchema = z
|
||||
scope: MediaUnderstandingScopeSchema,
|
||||
maxBytes: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
prompt: z.string().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
language: z.string().optional(),
|
||||
providerOptions: ProviderOptionsSchema,
|
||||
deepgram: DeepgramAudioSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
...MediaUnderstandingRuntimeFields,
|
||||
attachments: MediaUnderstandingAttachmentsSchema,
|
||||
models: z.array(MediaUnderstandingModelSchema).optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user