mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 20:51:10 +00:00
refactor(config): drop stale legacy migrations
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Breaking
|
||||
|
||||
- Providers/Qwen: remove the deprecated `qwen-portal-auth` OAuth integration for `portal.qwen.ai`; migrate to Model Studio with `openclaw onboard --auth-choice modelstudio-api-key`. (#52709) Thanks @pomelo-nwu.
|
||||
- Config/Doctor: drop automatic config migrations older than two months; very old legacy keys now fail validation instead of being rewritten on load or by `openclaw doctor`.
|
||||
|
||||
### Changes
|
||||
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDoctorRuntime,
|
||||
findLegacyGatewayServices,
|
||||
migrateLegacyConfig,
|
||||
mockDoctorConfigSnapshot,
|
||||
note,
|
||||
readConfigFileSnapshot,
|
||||
resolveOpenClawPackageRoot,
|
||||
runCommandWithTimeout,
|
||||
runGatewayUpdate,
|
||||
serviceInstall,
|
||||
serviceIsLoaded,
|
||||
uninstallLegacyGatewayServices,
|
||||
writeConfigFile,
|
||||
} from "./doctor.e2e-harness.js";
|
||||
import "./doctor.fast-path-mocks.js";
|
||||
|
||||
const DOCTOR_MIGRATION_TIMEOUT_MS = process.platform === "win32" ? 60_000 : 45_000;
|
||||
const { doctorCommand } = await import("./doctor.js");
|
||||
|
||||
describe("doctor command", () => {
|
||||
it("does not add a new gateway auth token while fixing legacy issues on invalid config", async () => {
|
||||
mockDoctorConfigSnapshot({
|
||||
config: {
|
||||
routing: { allowFrom: ["+15555550123"] },
|
||||
gateway: { remote: { token: "legacy-remote-token" } },
|
||||
},
|
||||
parsed: {
|
||||
routing: { allowFrom: ["+15555550123"] },
|
||||
gateway: { remote: { token: "legacy-remote-token" } },
|
||||
},
|
||||
valid: false,
|
||||
issues: [{ path: "routing.allowFrom", message: "legacy" }],
|
||||
legacyIssues: [{ path: "routing.allowFrom", message: "legacy" }],
|
||||
});
|
||||
|
||||
const runtime = createDoctorRuntime();
|
||||
|
||||
migrateLegacyConfig.mockReturnValue({
|
||||
config: {
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
gateway: { remote: { token: "legacy-remote-token" } },
|
||||
},
|
||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||
});
|
||||
|
||||
await doctorCommand(runtime, { repair: true });
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
const gateway = (written.gateway as Record<string, unknown>) ?? {};
|
||||
const auth = gateway.auth as Record<string, unknown> | undefined;
|
||||
const remote = gateway.remote as Record<string, unknown>;
|
||||
const channels = (written.channels as Record<string, unknown>) ?? {};
|
||||
|
||||
expect(channels.whatsapp).toEqual(
|
||||
expect.objectContaining({
|
||||
allowFrom: ["+15555550123"],
|
||||
}),
|
||||
);
|
||||
expect(written.routing).toBeUndefined();
|
||||
expect(remote.token).toBe("legacy-remote-token");
|
||||
expect(auth).toBeUndefined();
|
||||
});
|
||||
|
||||
it(
|
||||
"skips legacy gateway services migration",
|
||||
{ timeout: DOCTOR_MIGRATION_TIMEOUT_MS },
|
||||
async () => {
|
||||
mockDoctorConfigSnapshot();
|
||||
|
||||
findLegacyGatewayServices.mockResolvedValueOnce([
|
||||
{
|
||||
platform: "darwin",
|
||||
label: "com.steipete.openclaw.gateway",
|
||||
detail: "loaded",
|
||||
},
|
||||
]);
|
||||
serviceIsLoaded.mockResolvedValueOnce(false);
|
||||
serviceInstall.mockClear();
|
||||
|
||||
await doctorCommand(createDoctorRuntime());
|
||||
|
||||
expect(uninstallLegacyGatewayServices).not.toHaveBeenCalled();
|
||||
expect(serviceInstall).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("offers to update first for git checkouts", async () => {
|
||||
delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
|
||||
const root = "/tmp/openclaw";
|
||||
resolveOpenClawPackageRoot.mockResolvedValueOnce(root);
|
||||
runCommandWithTimeout.mockResolvedValueOnce({
|
||||
stdout: `${root}\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
runGatewayUpdate.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
mode: "git",
|
||||
root,
|
||||
steps: [],
|
||||
durationMs: 1,
|
||||
});
|
||||
|
||||
mockDoctorConfigSnapshot();
|
||||
|
||||
await doctorCommand(createDoctorRuntime());
|
||||
|
||||
expect(runGatewayUpdate).toHaveBeenCalledWith(expect.objectContaining({ cwd: root }));
|
||||
expect(readConfigFileSnapshot).not.toHaveBeenCalled();
|
||||
expect(
|
||||
note.mock.calls.some(([, title]) => typeof title === "string" && title === "Update result"),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue } from "./model-input.js";
|
||||
|
||||
const { loadConfig, migrateLegacyConfig, readConfigFileSnapshot, validateConfigObject } =
|
||||
await vi.importActual<typeof import("./config.js")>("./config.js");
|
||||
@@ -64,18 +63,15 @@ function expectInvalidIssuePath(config: unknown, expectedPath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function expectRoutingAllowFromLegacySnapshot(
|
||||
function expectSnapshotInvalidRootKey(
|
||||
ctx: { snapshot: ConfigSnapshot; parsed: unknown },
|
||||
expectedAllowFrom: string[],
|
||||
key: string,
|
||||
) {
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
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();
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.legacyIssues).toEqual([]);
|
||||
expect(ctx.snapshot.issues[0]?.path).toBe("");
|
||||
expect(ctx.snapshot.issues[0]?.message).toContain(`"${key}"`);
|
||||
expect((ctx.parsed as Record<string, unknown>)[key]).toBeTruthy();
|
||||
}
|
||||
|
||||
describe("legacy config detection", () => {
|
||||
@@ -207,30 +203,25 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path === "agent.model")).toBe(true);
|
||||
expect(res.issues[0]?.path).toBe("");
|
||||
expect(res.issues[0]?.message).toContain('"agent"');
|
||||
}
|
||||
});
|
||||
it("migrates telegram.requireMention to channels.telegram.groups.*.requireMention", async () => {
|
||||
it("does not rewrite removed telegram.requireMention migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
telegram: { requireMention: false },
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(
|
||||
(res.config?.channels?.telegram as { requireMention?: boolean } | undefined)?.requireMention,
|
||||
).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates messages.tts.enabled to messages.tts.auto", async () => {
|
||||
it("does not rewrite removed messages.tts.enabled migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
messages: { tts: { enabled: true } },
|
||||
});
|
||||
expect(res.changes).toContain("Moved messages.tts.enabled → messages.tts.auto (always).");
|
||||
expect(res.config?.messages?.tts?.auto).toBe("always");
|
||||
expect(res.config?.messages?.tts?.enabled).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates legacy model config to agent.models + model lists", async () => {
|
||||
it("does not rewrite removed legacy model config migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
@@ -241,28 +232,12 @@ describe("legacy config detection", () => {
|
||||
modelAliases: { Opus: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveAgentModelPrimaryValue(res.config?.agents?.defaults?.model)).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(resolveAgentModelFallbackValues(res.config?.agents?.defaults?.model)).toEqual([
|
||||
"openai/gpt-4.1-mini",
|
||||
]);
|
||||
expect(resolveAgentModelPrimaryValue(res.config?.agents?.defaults?.imageModel)).toBe(
|
||||
"openai/gpt-4.1-mini",
|
||||
);
|
||||
expect(resolveAgentModelFallbackValues(res.config?.agents?.defaults?.imageModel)).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
]);
|
||||
expect(res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]).toMatchObject({
|
||||
alias: "Opus",
|
||||
});
|
||||
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||
expect((res.config as { agent?: unknown } | undefined)?.agent).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("flags legacy config in snapshot", async () => {
|
||||
it("rejects removed routing.allowFrom in snapshot", async () => {
|
||||
await withSnapshotForConfig({ routing: { allowFrom: ["+15555550123"] } }, async (ctx) => {
|
||||
expectRoutingAllowFromLegacySnapshot(ctx, ["+15555550123"]);
|
||||
expectSnapshotInvalidRootKey(ctx, "routing");
|
||||
});
|
||||
});
|
||||
it("flags top-level memorySearch as legacy in snapshot", async () => {
|
||||
@@ -283,17 +258,9 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
it("flags legacy provider sections in snapshot", async () => {
|
||||
it("rejects removed legacy provider sections in snapshot", async () => {
|
||||
await withSnapshotForConfig({ whatsapp: { allowFrom: ["+1555"] } }, async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
|
||||
|
||||
const parsed = ctx.parsed as {
|
||||
channels?: unknown;
|
||||
whatsapp?: unknown;
|
||||
};
|
||||
expect(parsed.channels).toBeUndefined();
|
||||
expect(parsed.whatsapp).toBeTruthy();
|
||||
expectSnapshotInvalidRootKey(ctx, "whatsapp");
|
||||
});
|
||||
});
|
||||
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
|
||||
@@ -326,9 +293,18 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||
});
|
||||
});
|
||||
it("flags routing.allowFrom in snapshot", async () => {
|
||||
it("still flags memorySearch in snapshot under the shorter support window", async () => {
|
||||
await withSnapshotForConfig(
|
||||
{ memorySearch: { provider: "local", fallback: "none" } },
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
it("rejects removed routing.allowFrom in snapshot with other values", async () => {
|
||||
await withSnapshotForConfig({ routing: { allowFrom: ["+1666"] } }, async (ctx) => {
|
||||
expectRoutingAllowFromLegacySnapshot(ctx, ["+1666"]);
|
||||
expectSnapshotInvalidRootKey(ctx, "routing");
|
||||
});
|
||||
});
|
||||
it("rejects bindings[].match.provider on load", async () => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
import { migrateLegacyConfig, validateConfigObject } from "./config.js";
|
||||
import { WHISPER_BASE_AUDIO_MODEL } from "./legacy-migrate.test-helpers.js";
|
||||
|
||||
function getLegacyRouting(config: unknown) {
|
||||
return (config as { routing?: Record<string, unknown> } | undefined)?.routing;
|
||||
}
|
||||
|
||||
function getChannelConfig(config: unknown, provider: string) {
|
||||
const channels = (config as { channels?: Record<string, Record<string, unknown>> } | undefined)
|
||||
@@ -19,12 +14,14 @@ describe("legacy config detection", () => {
|
||||
{
|
||||
name: "routing.allowFrom",
|
||||
input: { routing: { allowFrom: ["+15555550123"] } },
|
||||
expectedPath: "routing.allowFrom",
|
||||
expectedPath: "",
|
||||
expectedMessage: '"routing"',
|
||||
},
|
||||
{
|
||||
name: "routing.groupChat.requireMention",
|
||||
input: { routing: { groupChat: { requireMention: false } } },
|
||||
expectedPath: "routing.groupChat.requireMention",
|
||||
expectedPath: "",
|
||||
expectedMessage: '"routing"',
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
@@ -32,91 +29,36 @@ describe("legacy config detection", () => {
|
||||
expect(res.ok, testCase.name).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path, testCase.name).toBe(testCase.expectedPath);
|
||||
expect(res.issues[0]?.message, testCase.name).toContain(testCase.expectedMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("migrates or drops routing.allowFrom based on whatsapp configuration", async () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "whatsapp configured",
|
||||
input: { routing: { allowFrom: ["+15555550123"] }, channels: { whatsapp: {} } },
|
||||
expectedChange: "Moved routing.allowFrom → channels.whatsapp.allowFrom.",
|
||||
expectWhatsappAllowFrom: true,
|
||||
},
|
||||
{
|
||||
name: "whatsapp missing",
|
||||
input: { routing: { allowFrom: ["+15555550123"] } },
|
||||
expectedChange: "Removed routing.allowFrom (channels.whatsapp not configured).",
|
||||
expectWhatsappAllowFrom: false,
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
const res = migrateLegacyConfig(testCase.input);
|
||||
expect(res.changes, testCase.name).toContain(testCase.expectedChange);
|
||||
if (testCase.expectWhatsappAllowFrom) {
|
||||
expect(res.config?.channels?.whatsapp?.allowFrom, testCase.name).toEqual(["+15555550123"]);
|
||||
} else {
|
||||
expect(res.config?.channels?.whatsapp, testCase.name).toBeUndefined();
|
||||
}
|
||||
expect(getLegacyRouting(res.config)?.allowFrom, testCase.name).toBeUndefined();
|
||||
}
|
||||
it("does not rewrite removed routing.allowFrom migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: { allowFrom: ["+15555550123"] },
|
||||
channels: { whatsapp: {} },
|
||||
});
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
|
||||
it("migrates routing.groupChat.requireMention to provider group defaults", async () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "whatsapp configured",
|
||||
input: { routing: { groupChat: { requireMention: false } }, channels: { whatsapp: {} } },
|
||||
expectWhatsapp: true,
|
||||
},
|
||||
{
|
||||
name: "whatsapp missing",
|
||||
input: { routing: { groupChat: { requireMention: false } } },
|
||||
expectWhatsapp: false,
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
const res = migrateLegacyConfig(testCase.input);
|
||||
expect(res.changes, testCase.name).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.changes, testCase.name).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
||||
);
|
||||
if (testCase.expectWhatsapp) {
|
||||
expect(res.changes, testCase.name).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.whatsapp.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.whatsapp?.groups?.["*"]?.requireMention, testCase.name).toBe(
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
expect(res.changes, testCase.name).not.toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.whatsapp.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.whatsapp, testCase.name).toBeUndefined();
|
||||
}
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention, testCase.name).toBe(
|
||||
false,
|
||||
);
|
||||
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention, testCase.name).toBe(
|
||||
false,
|
||||
);
|
||||
expect(getLegacyRouting(res.config)?.groupChat, testCase.name).toBeUndefined();
|
||||
}
|
||||
it("does not rewrite removed routing.groupChat.requireMention migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: { groupChat: { requireMention: false } },
|
||||
channels: { whatsapp: {} },
|
||||
});
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates routing.groupChat.mentionPatterns to messages.groupChat.mentionPatterns", async () => {
|
||||
it("does not rewrite removed routing.groupChat.mentionPatterns migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: { groupChat: { mentionPatterns: ["@openclaw"] } },
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
);
|
||||
expect(res.config?.messages?.groupChat?.mentionPatterns).toEqual(["@openclaw"]);
|
||||
expect(getLegacyRouting(res.config)?.groupChat).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates routing agentToAgent/queue/transcribeAudio to tools/messages/media", async () => {
|
||||
it("does not rewrite removed routing agentToAgent/queue/transcribeAudio migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
agentToAgent: { enabled: true, allow: ["main"] },
|
||||
@@ -127,19 +69,8 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain("Moved routing.agentToAgent → tools.agentToAgent.");
|
||||
expect(res.changes).toContain("Moved routing.queue → messages.queue.");
|
||||
expect(res.changes).toContain("Moved routing.transcribeAudio → tools.media.audio.models.");
|
||||
expect(res.config?.tools?.agentToAgent).toEqual({
|
||||
enabled: true,
|
||||
allow: ["main"],
|
||||
});
|
||||
expect(res.config?.messages?.queue).toEqual({
|
||||
mode: "queue",
|
||||
cap: 3,
|
||||
});
|
||||
expect(res.config?.tools?.media?.audio).toEqual(WHISPER_BASE_AUDIO_MODEL);
|
||||
expect(getLegacyRouting(res.config)).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates audio.transcription with custom script names", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
@@ -176,7 +107,7 @@ describe("legacy config detection", () => {
|
||||
expect(res.config?.tools?.media?.audio).toBeUndefined();
|
||||
expect(res.config?.audio).toBeUndefined();
|
||||
});
|
||||
it("migrates agent config into agents.defaults and tools", async () => {
|
||||
it("does not rewrite removed agent config migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "openai/gpt-5.2",
|
||||
@@ -187,31 +118,8 @@ describe("legacy config detection", () => {
|
||||
subagents: { tools: { deny: ["sandbox"] } },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain("Moved agent.tools.allow → tools.allow.");
|
||||
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
|
||||
expect(res.changes).toContain("Moved agent.elevated → tools.elevated.");
|
||||
expect(res.changes).toContain("Moved agent.bash → tools.exec.");
|
||||
expect(res.changes).toContain("Moved agent.sandbox.tools → tools.sandbox.tools.");
|
||||
expect(res.changes).toContain("Moved agent.subagents.tools → tools.subagents.tools.");
|
||||
expect(res.changes).toContain("Moved agent → agents.defaults.");
|
||||
expect(res.config?.agents?.defaults?.model).toEqual({
|
||||
primary: "openai/gpt-5.2",
|
||||
fallbacks: [],
|
||||
});
|
||||
expect(res.config?.tools?.allow).toEqual(["sessions.list"]);
|
||||
expect(res.config?.tools?.deny).toEqual(["danger"]);
|
||||
expect(res.config?.tools?.elevated).toEqual({
|
||||
enabled: true,
|
||||
allowFrom: { discord: ["user:1"] },
|
||||
});
|
||||
expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 });
|
||||
expect(res.config?.tools?.sandbox?.tools).toEqual({
|
||||
allow: ["browser.open"],
|
||||
});
|
||||
expect(res.config?.tools?.subagents?.tools).toEqual({
|
||||
deny: ["sandbox"],
|
||||
});
|
||||
expect((res.config as { agent?: unknown }).agent).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("migrates top-level memorySearch to agents.defaults.memorySearch", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
@@ -284,15 +192,14 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
it("migrates tools.bash to tools.exec", async () => {
|
||||
it("does not rewrite removed tools.bash migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
tools: {
|
||||
bash: { timeoutSec: 12 },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain("Moved tools.bash → tools.exec.");
|
||||
expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 });
|
||||
expect((res.config?.tools as { bash?: unknown } | undefined)?.bash).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("accepts per-agent tools.elevated overrides", async () => {
|
||||
const res = validateConfigObject({
|
||||
@@ -330,7 +237,8 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((issue) => issue.path === "telegram.requireMention")).toBe(true);
|
||||
expect(res.issues[0]?.path).toBe("");
|
||||
expect(res.issues[0]?.message).toContain('"telegram"');
|
||||
}
|
||||
});
|
||||
it("rejects gateway.token", async () => {
|
||||
@@ -339,17 +247,15 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe("gateway.token");
|
||||
expect(res.issues[0]?.path).toBe("gateway");
|
||||
}
|
||||
});
|
||||
it("migrates gateway.token to gateway.auth.token", async () => {
|
||||
it("does not rewrite removed gateway.token migrations", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
gateway: { token: "legacy-token" },
|
||||
});
|
||||
expect(res.changes).toContain("Moved gateway.token → gateway.auth.token.");
|
||||
expect(res.config?.gateway?.auth?.token).toBe("legacy-token");
|
||||
expect(res.config?.gateway?.auth?.mode).toBe("token");
|
||||
expect((res.config?.gateway as { token?: string })?.token).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
it("keeps gateway.bind tailnet", async () => {
|
||||
const res = migrateLegacyConfig({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyConfig } from "./legacy-migrate.js";
|
||||
import { WHISPER_BASE_AUDIO_MODEL } from "./legacy-migrate.test-helpers.js";
|
||||
|
||||
describe("legacy migrate audio transcription", () => {
|
||||
it("moves routing.transcribeAudio into tools.media.audio.models", () => {
|
||||
it("does not rewrite removed routing.transcribeAudio migrations", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
transcribeAudio: {
|
||||
@@ -13,12 +12,11 @@ describe("legacy migrate audio transcription", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain("Moved routing.transcribeAudio → tools.media.audio.models.");
|
||||
expect(res.config?.tools?.media?.audio).toEqual(WHISPER_BASE_AUDIO_MODEL);
|
||||
expect((res.config as { routing?: unknown } | null)?.routing).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps existing tools media model and drops legacy routing value", () => {
|
||||
it("does not rewrite removed routing.transcribeAudio migrations when new config exists", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
transcribeAudio: {
|
||||
@@ -34,11 +32,8 @@ describe("legacy migrate audio transcription", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
"Removed routing.transcribeAudio (tools.media.audio.models already set).",
|
||||
);
|
||||
expect(res.config?.tools?.media?.audio?.models).toEqual([{ command: "existing", type: "cli" }]);
|
||||
expect((res.config as { routing?: unknown } | null)?.routing).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
|
||||
it("drops invalid audio.transcription payloads", () => {
|
||||
@@ -57,7 +52,7 @@ describe("legacy migrate audio transcription", () => {
|
||||
});
|
||||
|
||||
describe("legacy migrate mention routing", () => {
|
||||
it("moves routing.groupChat.requireMention into channel group defaults", () => {
|
||||
it("does not rewrite removed routing.groupChat.requireMention migrations", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
groupChat: {
|
||||
@@ -66,18 +61,11 @@ describe("legacy migrate mention routing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(true);
|
||||
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention).toBe(true);
|
||||
expect((res.config as { routing?: unknown } | null)?.routing).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
|
||||
it("moves channels.telegram.requireMention into groups.*.requireMention", () => {
|
||||
it("does not rewrite removed channels.telegram.requireMention migrations", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
channels: {
|
||||
telegram: {
|
||||
@@ -86,13 +74,8 @@ describe("legacy migrate mention routing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(
|
||||
(res.config?.channels?.telegram as { requireMention?: unknown } | undefined)?.requireMention,
|
||||
).toBeUndefined();
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,12 +168,14 @@ describe("legacy migrate heartbeat config", () => {
|
||||
expect((res.config as { heartbeat?: unknown } | null)?.heartbeat).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves agent.heartbeat precedence over top-level heartbeat legacy key", () => {
|
||||
it("preserves agents.defaults.heartbeat precedence over top-level heartbeat legacy key", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
heartbeat: {
|
||||
every: "1h",
|
||||
target: "telegram",
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "1h",
|
||||
target: "telegram",
|
||||
},
|
||||
},
|
||||
},
|
||||
heartbeat: {
|
||||
@@ -206,7 +191,6 @@ describe("legacy migrate heartbeat config", () => {
|
||||
model: "anthropic/claude-3-5-haiku-20241022",
|
||||
});
|
||||
expect((res.config as { heartbeat?: unknown } | null)?.heartbeat).toBeUndefined();
|
||||
expect((res.config as { agent?: unknown } | null)?.agent).toBeUndefined();
|
||||
});
|
||||
|
||||
it("drops blocked prototype keys when migrating top-level heartbeat", () => {
|
||||
|
||||
@@ -6,56 +6,7 @@ import {
|
||||
resolveSlackStreamingMode,
|
||||
resolveTelegramPreviewStreamMode,
|
||||
} from "./discord-preview-streaming.js";
|
||||
import {
|
||||
ensureRecord,
|
||||
getRecord,
|
||||
isRecord,
|
||||
type LegacyConfigMigration,
|
||||
mergeMissing,
|
||||
} from "./legacy.shared.js";
|
||||
|
||||
function migrateBindings(
|
||||
raw: Record<string, unknown>,
|
||||
changes: string[],
|
||||
changeNote: string,
|
||||
mutator: (match: Record<string, unknown>) => boolean,
|
||||
) {
|
||||
const bindings = Array.isArray(raw.bindings) ? raw.bindings : null;
|
||||
if (!bindings) {
|
||||
return;
|
||||
}
|
||||
|
||||
let touched = false;
|
||||
for (const entry of bindings) {
|
||||
if (!isRecord(entry)) {
|
||||
continue;
|
||||
}
|
||||
const match = getRecord(entry.match);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
if (!mutator(match)) {
|
||||
continue;
|
||||
}
|
||||
entry.match = match;
|
||||
touched = true;
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
raw.bindings = bindings;
|
||||
changes.push(changeNote);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDefaultGroupEntry(section: Record<string, unknown>): {
|
||||
groups: Record<string, unknown>;
|
||||
entry: Record<string, unknown>;
|
||||
} {
|
||||
const groups: Record<string, unknown> = isRecord(section.groups) ? section.groups : {};
|
||||
const defaultKey = "*";
|
||||
const entry: Record<string, unknown> = isRecord(groups[defaultKey]) ? groups[defaultKey] : {};
|
||||
return { groups, entry };
|
||||
}
|
||||
import { getRecord, type LegacyConfigMigration } from "./legacy.shared.js";
|
||||
|
||||
function hasOwnKey(target: Record<string, unknown>, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
@@ -95,162 +46,6 @@ function migrateThreadBindingsTtlHoursForPath(params: {
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
{
|
||||
id: "bindings.match.provider->bindings.match.channel",
|
||||
describe: "Move bindings[].match.provider to bindings[].match.channel",
|
||||
apply: (raw, changes) => {
|
||||
migrateBindings(
|
||||
raw,
|
||||
changes,
|
||||
"Moved bindings[].match.provider → bindings[].match.channel.",
|
||||
(match) => {
|
||||
if (typeof match.channel === "string" && match.channel.trim()) {
|
||||
return false;
|
||||
}
|
||||
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
match.channel = provider;
|
||||
delete match.provider;
|
||||
return true;
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bindings.match.accountID->bindings.match.accountId",
|
||||
describe: "Move bindings[].match.accountID to bindings[].match.accountId",
|
||||
apply: (raw, changes) => {
|
||||
migrateBindings(
|
||||
raw,
|
||||
changes,
|
||||
"Moved bindings[].match.accountID → bindings[].match.accountId.",
|
||||
(match) => {
|
||||
if (match.accountId !== undefined) {
|
||||
return false;
|
||||
}
|
||||
const accountID =
|
||||
typeof match.accountID === "string" ? match.accountID.trim() : match.accountID;
|
||||
if (!accountID) {
|
||||
return false;
|
||||
}
|
||||
match.accountId = accountID;
|
||||
delete match.accountID;
|
||||
return true;
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.sendPolicy.rules.match.provider->match.channel",
|
||||
describe: "Move session.sendPolicy.rules[].match.provider to match.channel",
|
||||
apply: (raw, changes) => {
|
||||
const session = getRecord(raw.session);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const sendPolicy = getRecord(session.sendPolicy);
|
||||
if (!sendPolicy) {
|
||||
return;
|
||||
}
|
||||
const rules = Array.isArray(sendPolicy.rules) ? sendPolicy.rules : null;
|
||||
if (!rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
let touched = false;
|
||||
for (const rule of rules) {
|
||||
if (!isRecord(rule)) {
|
||||
continue;
|
||||
}
|
||||
const match = getRecord(rule.match);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
if (typeof match.channel === "string" && match.channel.trim()) {
|
||||
continue;
|
||||
}
|
||||
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
match.channel = provider;
|
||||
delete match.provider;
|
||||
rule.match = match;
|
||||
touched = true;
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
sendPolicy.rules = rules;
|
||||
session.sendPolicy = sendPolicy;
|
||||
raw.session = session;
|
||||
changes.push("Moved session.sendPolicy.rules[].match.provider → match.channel.");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "messages.queue.byProvider->byChannel",
|
||||
describe: "Move messages.queue.byProvider to messages.queue.byChannel",
|
||||
apply: (raw, changes) => {
|
||||
const messages = getRecord(raw.messages);
|
||||
if (!messages) {
|
||||
return;
|
||||
}
|
||||
const queue = getRecord(messages.queue);
|
||||
if (!queue) {
|
||||
return;
|
||||
}
|
||||
if (queue.byProvider === undefined) {
|
||||
return;
|
||||
}
|
||||
if (queue.byChannel === undefined) {
|
||||
queue.byChannel = queue.byProvider;
|
||||
changes.push("Moved messages.queue.byProvider → messages.queue.byChannel.");
|
||||
} else {
|
||||
changes.push("Removed messages.queue.byProvider (messages.queue.byChannel already set).");
|
||||
}
|
||||
delete queue.byProvider;
|
||||
messages.queue = queue;
|
||||
raw.messages = messages;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "providers->channels",
|
||||
describe: "Move provider config sections to channels.*",
|
||||
apply: (raw, changes) => {
|
||||
const legacyKeys = [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"msteams",
|
||||
];
|
||||
const legacyEntries = legacyKeys.filter((key) => isRecord(raw[key]));
|
||||
if (legacyEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = ensureRecord(raw, "channels");
|
||||
for (const key of legacyEntries) {
|
||||
const legacy = getRecord(raw[key]);
|
||||
if (!legacy) {
|
||||
continue;
|
||||
}
|
||||
const channelEntry = ensureRecord(channels, key);
|
||||
const hadEntries = Object.keys(channelEntry).length > 0;
|
||||
mergeMissing(channelEntry, legacy);
|
||||
channels[key] = channelEntry;
|
||||
delete raw[key];
|
||||
changes.push(
|
||||
hadEntries ? `Merged ${key} → channels.${key}.` : `Moved ${key} → channels.${key}.`,
|
||||
);
|
||||
}
|
||||
raw.channels = channels;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "thread-bindings.ttlHours->idleHours",
|
||||
describe:
|
||||
@@ -402,143 +197,6 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
migrateProvider("slack");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.allowFrom->channels.whatsapp.allowFrom",
|
||||
describe: "Move routing.allowFrom to channels.whatsapp.allowFrom",
|
||||
apply: (raw, changes) => {
|
||||
const routing = raw.routing;
|
||||
if (!routing || typeof routing !== "object") {
|
||||
return;
|
||||
}
|
||||
const allowFrom = (routing as Record<string, unknown>).allowFrom;
|
||||
if (allowFrom === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = getRecord(raw.channels);
|
||||
const whatsapp = channels ? getRecord(channels.whatsapp) : null;
|
||||
if (!whatsapp) {
|
||||
delete (routing as Record<string, unknown>).allowFrom;
|
||||
if (Object.keys(routing as Record<string, unknown>).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
changes.push("Removed routing.allowFrom (channels.whatsapp not configured).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (whatsapp.allowFrom === undefined) {
|
||||
whatsapp.allowFrom = allowFrom;
|
||||
changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
|
||||
} else {
|
||||
changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set).");
|
||||
}
|
||||
|
||||
delete (routing as Record<string, unknown>).allowFrom;
|
||||
if (Object.keys(routing as Record<string, unknown>).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
channels!.whatsapp = whatsapp;
|
||||
raw.channels = channels!;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.groupChat.requireMention->groups.*.requireMention",
|
||||
describe: "Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups",
|
||||
apply: (raw, changes) => {
|
||||
const routing = raw.routing;
|
||||
if (!routing || typeof routing !== "object") {
|
||||
return;
|
||||
}
|
||||
const groupChat =
|
||||
(routing as Record<string, unknown>).groupChat &&
|
||||
typeof (routing as Record<string, unknown>).groupChat === "object"
|
||||
? ((routing as Record<string, unknown>).groupChat as Record<string, unknown>)
|
||||
: null;
|
||||
if (!groupChat) {
|
||||
return;
|
||||
}
|
||||
const requireMention = groupChat.requireMention;
|
||||
if (requireMention === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = ensureRecord(raw, "channels");
|
||||
const applyTo = (
|
||||
key: "whatsapp" | "telegram" | "imessage",
|
||||
options?: { requireExisting?: boolean },
|
||||
) => {
|
||||
if (options?.requireExisting && !isRecord(channels[key])) {
|
||||
return;
|
||||
}
|
||||
const section =
|
||||
channels[key] && typeof channels[key] === "object"
|
||||
? (channels[key] as Record<string, unknown>)
|
||||
: {};
|
||||
const { groups, entry } = ensureDefaultGroupEntry(section);
|
||||
const defaultKey = "*";
|
||||
if (entry.requireMention === undefined) {
|
||||
entry.requireMention = requireMention;
|
||||
groups[defaultKey] = entry;
|
||||
section.groups = groups;
|
||||
channels[key] = section;
|
||||
changes.push(
|
||||
`Moved routing.groupChat.requireMention → channels.${key}.groups."*".requireMention.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
`Removed routing.groupChat.requireMention (channels.${key}.groups."*" already set).`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
applyTo("whatsapp", { requireExisting: true });
|
||||
applyTo("telegram");
|
||||
applyTo("imessage");
|
||||
|
||||
delete groupChat.requireMention;
|
||||
if (Object.keys(groupChat).length === 0) {
|
||||
delete (routing as Record<string, unknown>).groupChat;
|
||||
}
|
||||
if (Object.keys(routing as Record<string, unknown>).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
raw.channels = channels;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gateway.token->gateway.auth.token",
|
||||
describe: "Move gateway.token to gateway.auth.token",
|
||||
apply: (raw, changes) => {
|
||||
const gateway = raw.gateway;
|
||||
if (!gateway || typeof gateway !== "object") {
|
||||
return;
|
||||
}
|
||||
const token = (gateway as Record<string, unknown>).token;
|
||||
if (token === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gatewayObj = gateway as Record<string, unknown>;
|
||||
const auth =
|
||||
gatewayObj.auth && typeof gatewayObj.auth === "object"
|
||||
? (gatewayObj.auth as Record<string, unknown>)
|
||||
: {};
|
||||
if (auth.token === undefined) {
|
||||
auth.token = token;
|
||||
if (!auth.mode) {
|
||||
auth.mode = "token";
|
||||
}
|
||||
changes.push("Moved gateway.token → gateway.auth.token.");
|
||||
} else {
|
||||
changes.push("Removed gateway.token (gateway.auth.token already set).");
|
||||
}
|
||||
delete gatewayObj.token;
|
||||
if (Object.keys(auth).length > 0) {
|
||||
gatewayObj.auth = auth;
|
||||
}
|
||||
raw.gateway = gatewayObj;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gateway.bind.host-alias->bind-mode",
|
||||
describe: "Normalize gateway.bind host aliases to supported bind modes",
|
||||
@@ -579,37 +237,4 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
changes.push(`Normalized gateway.bind "${escapeControlForLog(bindRaw)}" → "${mapped}".`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "telegram.requireMention->channels.telegram.groups.*.requireMention",
|
||||
describe: "Move telegram.requireMention to channels.telegram.groups.*.requireMention",
|
||||
apply: (raw, changes) => {
|
||||
const channels = ensureRecord(raw, "channels");
|
||||
const telegram = channels.telegram;
|
||||
if (!telegram || typeof telegram !== "object") {
|
||||
return;
|
||||
}
|
||||
const requireMention = (telegram as Record<string, unknown>).requireMention;
|
||||
if (requireMention === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { groups, entry } = ensureDefaultGroupEntry(telegram as Record<string, unknown>);
|
||||
const defaultKey = "*";
|
||||
|
||||
if (entry.requireMention === undefined) {
|
||||
entry.requireMention = requireMention;
|
||||
groups[defaultKey] = entry;
|
||||
(telegram as Record<string, unknown>).groups = groups;
|
||||
changes.push(
|
||||
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
} else {
|
||||
changes.push('Removed telegram.requireMention (channels.telegram.groups."*" already set).');
|
||||
}
|
||||
|
||||
delete (telegram as Record<string, unknown>).requireMention;
|
||||
channels.telegram = telegram as Record<string, unknown>;
|
||||
raw.channels = channels;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import {
|
||||
ensureAgentEntry,
|
||||
ensureRecord,
|
||||
getAgentsList,
|
||||
getRecord,
|
||||
isRecord,
|
||||
type LegacyConfigMigration,
|
||||
mapLegacyAudioTranscription,
|
||||
mergeMissing,
|
||||
} from "./legacy.shared.js";
|
||||
|
||||
function applyLegacyAudioTranscriptionModel(params: {
|
||||
@@ -36,368 +32,6 @@ function applyLegacyAudioTranscriptionModel(params: {
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
{
|
||||
id: "agent.model-config-v2",
|
||||
describe:
|
||||
"Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
||||
apply: (raw, changes) => {
|
||||
const agentRoot = getRecord(raw.agent);
|
||||
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
||||
const agent = agentRoot ?? defaults;
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
const label = agentRoot ? "agent" : "agents.defaults";
|
||||
|
||||
const legacyModel = typeof agent.model === "string" ? String(agent.model) : undefined;
|
||||
const legacyImageModel =
|
||||
typeof agent.imageModel === "string" ? String(agent.imageModel) : undefined;
|
||||
const legacyAllowed = Array.isArray(agent.allowedModels)
|
||||
? (agent.allowedModels as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyModelFallbacks = Array.isArray(agent.modelFallbacks)
|
||||
? (agent.modelFallbacks as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyImageModelFallbacks = Array.isArray(agent.imageModelFallbacks)
|
||||
? (agent.imageModelFallbacks as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyAliases =
|
||||
agent.modelAliases && typeof agent.modelAliases === "object"
|
||||
? (agent.modelAliases as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const hasLegacy =
|
||||
legacyModel ||
|
||||
legacyImageModel ||
|
||||
legacyAllowed.length > 0 ||
|
||||
legacyModelFallbacks.length > 0 ||
|
||||
legacyImageModelFallbacks.length > 0 ||
|
||||
Object.keys(legacyAliases).length > 0;
|
||||
if (!hasLegacy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const models =
|
||||
agent.models && typeof agent.models === "object"
|
||||
? (agent.models as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const ensureModel = (rawKey?: string) => {
|
||||
if (typeof rawKey !== "string") {
|
||||
return;
|
||||
}
|
||||
const key = rawKey.trim();
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
if (!models[key]) {
|
||||
models[key] = {};
|
||||
}
|
||||
};
|
||||
|
||||
ensureModel(legacyModel);
|
||||
ensureModel(legacyImageModel);
|
||||
for (const key of legacyAllowed) {
|
||||
ensureModel(key);
|
||||
}
|
||||
for (const key of legacyModelFallbacks) {
|
||||
ensureModel(key);
|
||||
}
|
||||
for (const key of legacyImageModelFallbacks) {
|
||||
ensureModel(key);
|
||||
}
|
||||
for (const target of Object.values(legacyAliases)) {
|
||||
if (typeof target !== "string") {
|
||||
continue;
|
||||
}
|
||||
ensureModel(target);
|
||||
}
|
||||
|
||||
for (const [alias, targetRaw] of Object.entries(legacyAliases)) {
|
||||
if (typeof targetRaw !== "string") {
|
||||
continue;
|
||||
}
|
||||
const target = targetRaw.trim();
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
const entry =
|
||||
models[target] && typeof models[target] === "object"
|
||||
? (models[target] as Record<string, unknown>)
|
||||
: {};
|
||||
if (!("alias" in entry)) {
|
||||
entry.alias = alias;
|
||||
models[target] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
const currentModel =
|
||||
agent.model && typeof agent.model === "object"
|
||||
? (agent.model as Record<string, unknown>)
|
||||
: null;
|
||||
if (currentModel) {
|
||||
if (!currentModel.primary && legacyModel) {
|
||||
currentModel.primary = legacyModel;
|
||||
}
|
||||
if (
|
||||
legacyModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentModel.fallbacks) || currentModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentModel.fallbacks = legacyModelFallbacks;
|
||||
}
|
||||
agent.model = currentModel;
|
||||
} else if (legacyModel || legacyModelFallbacks.length > 0) {
|
||||
agent.model = {
|
||||
primary: legacyModel,
|
||||
fallbacks: legacyModelFallbacks.length ? legacyModelFallbacks : [],
|
||||
};
|
||||
}
|
||||
|
||||
const currentImageModel =
|
||||
agent.imageModel && typeof agent.imageModel === "object"
|
||||
? (agent.imageModel as Record<string, unknown>)
|
||||
: null;
|
||||
if (currentImageModel) {
|
||||
if (!currentImageModel.primary && legacyImageModel) {
|
||||
currentImageModel.primary = legacyImageModel;
|
||||
}
|
||||
if (
|
||||
legacyImageModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentImageModel.fallbacks) || currentImageModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentImageModel.fallbacks = legacyImageModelFallbacks;
|
||||
}
|
||||
agent.imageModel = currentImageModel;
|
||||
} else if (legacyImageModel || legacyImageModelFallbacks.length > 0) {
|
||||
agent.imageModel = {
|
||||
primary: legacyImageModel,
|
||||
fallbacks: legacyImageModelFallbacks.length ? legacyImageModelFallbacks : [],
|
||||
};
|
||||
}
|
||||
|
||||
agent.models = models;
|
||||
|
||||
if (legacyModel !== undefined) {
|
||||
changes.push(`Migrated ${label}.model string → ${label}.model.primary.`);
|
||||
}
|
||||
if (legacyModelFallbacks.length > 0) {
|
||||
changes.push(`Migrated ${label}.modelFallbacks → ${label}.model.fallbacks.`);
|
||||
}
|
||||
if (legacyImageModel !== undefined) {
|
||||
changes.push(`Migrated ${label}.imageModel string → ${label}.imageModel.primary.`);
|
||||
}
|
||||
if (legacyImageModelFallbacks.length > 0) {
|
||||
changes.push(`Migrated ${label}.imageModelFallbacks → ${label}.imageModel.fallbacks.`);
|
||||
}
|
||||
if (legacyAllowed.length > 0) {
|
||||
changes.push(`Migrated ${label}.allowedModels → ${label}.models.`);
|
||||
}
|
||||
if (Object.keys(legacyAliases).length > 0) {
|
||||
changes.push(`Migrated ${label}.modelAliases → ${label}.models.*.alias.`);
|
||||
}
|
||||
|
||||
delete agent.allowedModels;
|
||||
delete agent.modelAliases;
|
||||
delete agent.modelFallbacks;
|
||||
delete agent.imageModelFallbacks;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.agents-v2",
|
||||
describe: "Move routing.agents/defaultAgentId to agents.list",
|
||||
apply: (raw, changes) => {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const routingAgents = getRecord(routing.agents);
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const list = getAgentsList(agents);
|
||||
|
||||
if (routingAgents) {
|
||||
for (const [rawId, entryRaw] of Object.entries(routingAgents)) {
|
||||
const agentId = String(rawId ?? "").trim();
|
||||
const entry = getRecord(entryRaw);
|
||||
if (!agentId || !entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = ensureAgentEntry(list, agentId);
|
||||
const entryCopy: Record<string, unknown> = { ...entry };
|
||||
|
||||
if ("mentionPatterns" in entryCopy) {
|
||||
const mentionPatterns = entryCopy.mentionPatterns;
|
||||
const groupChat = ensureRecord(target, "groupChat");
|
||||
if (groupChat.mentionPatterns === undefined) {
|
||||
groupChat.mentionPatterns = mentionPatterns;
|
||||
changes.push(
|
||||
`Moved routing.agents.${agentId}.mentionPatterns → agents.list (id "${agentId}").groupChat.mentionPatterns.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
`Removed routing.agents.${agentId}.mentionPatterns (agents.list groupChat mentionPatterns already set).`,
|
||||
);
|
||||
}
|
||||
delete entryCopy.mentionPatterns;
|
||||
}
|
||||
|
||||
const legacyGroupChat = getRecord(entryCopy.groupChat);
|
||||
if (legacyGroupChat) {
|
||||
const groupChat = ensureRecord(target, "groupChat");
|
||||
mergeMissing(groupChat, legacyGroupChat);
|
||||
delete entryCopy.groupChat;
|
||||
}
|
||||
|
||||
const legacySandbox = getRecord(entryCopy.sandbox);
|
||||
if (legacySandbox) {
|
||||
const sandboxTools = getRecord(legacySandbox.tools);
|
||||
if (sandboxTools) {
|
||||
const tools = ensureRecord(target, "tools");
|
||||
const sandbox = ensureRecord(tools, "sandbox");
|
||||
const toolPolicy = ensureRecord(sandbox, "tools");
|
||||
mergeMissing(toolPolicy, sandboxTools);
|
||||
delete legacySandbox.tools;
|
||||
changes.push(
|
||||
`Moved routing.agents.${agentId}.sandbox.tools → agents.list (id "${agentId}").tools.sandbox.tools.`,
|
||||
);
|
||||
}
|
||||
entryCopy.sandbox = legacySandbox;
|
||||
}
|
||||
|
||||
mergeMissing(target, entryCopy);
|
||||
}
|
||||
delete routing.agents;
|
||||
changes.push("Moved routing.agents → agents.list.");
|
||||
}
|
||||
|
||||
const defaultAgentId =
|
||||
typeof routing.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
|
||||
if (defaultAgentId) {
|
||||
const hasDefault = list.some(
|
||||
(entry): entry is Record<string, unknown> => isRecord(entry) && entry.default === true,
|
||||
);
|
||||
if (!hasDefault) {
|
||||
const entry = ensureAgentEntry(list, defaultAgentId);
|
||||
entry.default = true;
|
||||
changes.push(
|
||||
`Moved routing.defaultAgentId → agents.list (id "${defaultAgentId}").default.`,
|
||||
);
|
||||
} else {
|
||||
changes.push("Removed routing.defaultAgentId (agents.list default already set).");
|
||||
}
|
||||
delete routing.defaultAgentId;
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
agents.list = list;
|
||||
}
|
||||
|
||||
if (Object.keys(routing).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "routing.config-v2",
|
||||
describe: "Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio",
|
||||
apply: (raw, changes) => {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (routing.bindings !== undefined) {
|
||||
if (raw.bindings === undefined) {
|
||||
raw.bindings = routing.bindings;
|
||||
changes.push("Moved routing.bindings → bindings.");
|
||||
} else {
|
||||
changes.push("Removed routing.bindings (bindings already set).");
|
||||
}
|
||||
delete routing.bindings;
|
||||
}
|
||||
|
||||
if (routing.agentToAgent !== undefined) {
|
||||
const tools = ensureRecord(raw, "tools");
|
||||
if (tools.agentToAgent === undefined) {
|
||||
tools.agentToAgent = routing.agentToAgent;
|
||||
changes.push("Moved routing.agentToAgent → tools.agentToAgent.");
|
||||
} else {
|
||||
changes.push("Removed routing.agentToAgent (tools.agentToAgent already set).");
|
||||
}
|
||||
delete routing.agentToAgent;
|
||||
}
|
||||
|
||||
if (routing.queue !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
if (messages.queue === undefined) {
|
||||
messages.queue = routing.queue;
|
||||
changes.push("Moved routing.queue → messages.queue.");
|
||||
} else {
|
||||
changes.push("Removed routing.queue (messages.queue already set).");
|
||||
}
|
||||
delete routing.queue;
|
||||
}
|
||||
|
||||
const groupChat = getRecord(routing.groupChat);
|
||||
if (groupChat) {
|
||||
const historyLimit = groupChat.historyLimit;
|
||||
if (historyLimit !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup.historyLimit === undefined) {
|
||||
messagesGroup.historyLimit = historyLimit;
|
||||
changes.push("Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.groupChat.historyLimit (messages.groupChat.historyLimit already set).",
|
||||
);
|
||||
}
|
||||
delete groupChat.historyLimit;
|
||||
}
|
||||
|
||||
const mentionPatterns = groupChat.mentionPatterns;
|
||||
if (mentionPatterns !== undefined) {
|
||||
const messages = ensureRecord(raw, "messages");
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup.mentionPatterns === undefined) {
|
||||
messagesGroup.mentionPatterns = mentionPatterns;
|
||||
changes.push(
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.groupChat.mentionPatterns (messages.groupChat.mentionPatterns already set).",
|
||||
);
|
||||
}
|
||||
delete groupChat.mentionPatterns;
|
||||
}
|
||||
|
||||
if (Object.keys(groupChat).length === 0) {
|
||||
delete routing.groupChat;
|
||||
} else {
|
||||
routing.groupChat = groupChat;
|
||||
}
|
||||
}
|
||||
|
||||
if (routing.transcribeAudio !== undefined) {
|
||||
applyLegacyAudioTranscriptionModel({
|
||||
raw,
|
||||
source: routing.transcribeAudio,
|
||||
changes,
|
||||
movedMessage: "Moved routing.transcribeAudio → tools.media.audio.models.",
|
||||
alreadySetMessage:
|
||||
"Removed routing.transcribeAudio (tools.media.audio.models already set).",
|
||||
invalidMessage: "Removed routing.transcribeAudio (invalid or empty command).",
|
||||
});
|
||||
delete routing.transcribeAudio;
|
||||
}
|
||||
|
||||
if (Object.keys(routing).length === 0) {
|
||||
delete raw.routing;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "audio.transcription-v2",
|
||||
describe: "Move audio.transcription to tools.media.audio.models",
|
||||
|
||||
@@ -5,14 +5,10 @@ import {
|
||||
resolveGatewayPortWithDefault,
|
||||
} from "./gateway-control-ui-origins.js";
|
||||
import {
|
||||
ensureAgentEntry,
|
||||
ensureRecord,
|
||||
getAgentsList,
|
||||
getRecord,
|
||||
isRecord,
|
||||
type LegacyConfigMigration,
|
||||
mergeMissing,
|
||||
resolveDefaultAgentIdFromRaw,
|
||||
} from "./legacy.shared.js";
|
||||
import { DEFAULT_GATEWAY_PORT } from "./paths.js";
|
||||
import { isBlockedObjectKey } from "./prototype-keys.js";
|
||||
@@ -215,69 +211,6 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
delete raw.memorySearch;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "auth.anthropic-claude-cli-mode-oauth",
|
||||
describe: "Switch anthropic:claude-cli auth profile mode to oauth",
|
||||
apply: (raw, changes) => {
|
||||
const auth = getRecord(raw.auth);
|
||||
const profiles = getRecord(auth?.profiles);
|
||||
if (!profiles) {
|
||||
return;
|
||||
}
|
||||
const claudeCli = getRecord(profiles["anthropic:claude-cli"]);
|
||||
if (!claudeCli) {
|
||||
return;
|
||||
}
|
||||
if (claudeCli.mode !== "token") {
|
||||
return;
|
||||
}
|
||||
claudeCli.mode = "oauth";
|
||||
changes.push('Updated auth.profiles["anthropic:claude-cli"].mode → "oauth".');
|
||||
},
|
||||
},
|
||||
// tools.alsoAllow migration removed (field not shipped in prod; enforce via schema instead).
|
||||
{
|
||||
id: "tools.bash->tools.exec",
|
||||
describe: "Move tools.bash to tools.exec",
|
||||
apply: (raw, changes) => {
|
||||
const tools = ensureRecord(raw, "tools");
|
||||
const bash = getRecord(tools.bash);
|
||||
if (!bash) {
|
||||
return;
|
||||
}
|
||||
if (tools.exec === undefined) {
|
||||
tools.exec = bash;
|
||||
changes.push("Moved tools.bash → tools.exec.");
|
||||
} else {
|
||||
changes.push("Removed tools.bash (tools.exec already set).");
|
||||
}
|
||||
delete tools.bash;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "messages.tts.enabled->auto",
|
||||
describe: "Move messages.tts.enabled to messages.tts.auto",
|
||||
apply: (raw, changes) => {
|
||||
const messages = getRecord(raw.messages);
|
||||
const tts = getRecord(messages?.tts);
|
||||
if (!tts) {
|
||||
return;
|
||||
}
|
||||
if (tts.auto !== undefined) {
|
||||
if ("enabled" in tts) {
|
||||
delete tts.enabled;
|
||||
changes.push("Removed messages.tts.enabled (messages.tts.auto already set).");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof tts.enabled !== "boolean") {
|
||||
return;
|
||||
}
|
||||
tts.auto = tts.enabled ? "always" : "off";
|
||||
delete tts.enabled;
|
||||
changes.push(`Moved messages.tts.enabled → messages.tts.auto (${String(tts.auto)}).`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tts.providers-generic-shape",
|
||||
describe: "Move legacy bundled TTS config keys into messages.tts.providers",
|
||||
@@ -308,93 +241,6 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agent.defaults-v2",
|
||||
describe: "Move agent config to agents.defaults and tools",
|
||||
apply: (raw, changes) => {
|
||||
const agent = getRecord(raw.agent);
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const defaults = getRecord(agents.defaults) ?? {};
|
||||
const tools = ensureRecord(raw, "tools");
|
||||
|
||||
const agentTools = getRecord(agent.tools);
|
||||
if (agentTools) {
|
||||
if (tools.allow === undefined && agentTools.allow !== undefined) {
|
||||
tools.allow = agentTools.allow;
|
||||
changes.push("Moved agent.tools.allow → tools.allow.");
|
||||
}
|
||||
if (tools.deny === undefined && agentTools.deny !== undefined) {
|
||||
tools.deny = agentTools.deny;
|
||||
changes.push("Moved agent.tools.deny → tools.deny.");
|
||||
}
|
||||
}
|
||||
|
||||
const elevated = getRecord(agent.elevated);
|
||||
if (elevated) {
|
||||
if (tools.elevated === undefined) {
|
||||
tools.elevated = elevated;
|
||||
changes.push("Moved agent.elevated → tools.elevated.");
|
||||
} else {
|
||||
changes.push("Removed agent.elevated (tools.elevated already set).");
|
||||
}
|
||||
}
|
||||
|
||||
const bash = getRecord(agent.bash);
|
||||
if (bash) {
|
||||
if (tools.exec === undefined) {
|
||||
tools.exec = bash;
|
||||
changes.push("Moved agent.bash → tools.exec.");
|
||||
} else {
|
||||
changes.push("Removed agent.bash (tools.exec already set).");
|
||||
}
|
||||
}
|
||||
|
||||
const sandbox = getRecord(agent.sandbox);
|
||||
if (sandbox) {
|
||||
const sandboxTools = getRecord(sandbox.tools);
|
||||
if (sandboxTools) {
|
||||
const toolsSandbox = ensureRecord(tools, "sandbox");
|
||||
const toolPolicy = ensureRecord(toolsSandbox, "tools");
|
||||
mergeMissing(toolPolicy, sandboxTools);
|
||||
delete sandbox.tools;
|
||||
changes.push("Moved agent.sandbox.tools → tools.sandbox.tools.");
|
||||
}
|
||||
}
|
||||
|
||||
const subagents = getRecord(agent.subagents);
|
||||
if (subagents) {
|
||||
const subagentTools = getRecord(subagents.tools);
|
||||
if (subagentTools) {
|
||||
const toolsSubagents = ensureRecord(tools, "subagents");
|
||||
const toolPolicy = ensureRecord(toolsSubagents, "tools");
|
||||
mergeMissing(toolPolicy, subagentTools);
|
||||
delete subagents.tools;
|
||||
changes.push("Moved agent.subagents.tools → tools.subagents.tools.");
|
||||
}
|
||||
}
|
||||
|
||||
const agentCopy: Record<string, unknown> = structuredClone(agent);
|
||||
delete agentCopy.tools;
|
||||
delete agentCopy.elevated;
|
||||
delete agentCopy.bash;
|
||||
if (isRecord(agentCopy.sandbox)) {
|
||||
delete agentCopy.sandbox.tools;
|
||||
}
|
||||
if (isRecord(agentCopy.subagents)) {
|
||||
delete agentCopy.subagents.tools;
|
||||
}
|
||||
|
||||
mergeMissing(defaults, agentCopy);
|
||||
agents.defaults = defaults;
|
||||
raw.agents = agents;
|
||||
delete raw.agent;
|
||||
changes.push("Moved agent → agents.defaults.");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "heartbeat->agents.defaults.heartbeat",
|
||||
describe: "Move top-level heartbeat to agents.defaults.heartbeat/channels.defaults.heartbeat",
|
||||
@@ -438,28 +284,4 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
delete raw.heartbeat;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "identity->agents.list",
|
||||
describe: "Move identity to agents.list[].identity",
|
||||
apply: (raw, changes) => {
|
||||
const identity = getRecord(raw.identity);
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = ensureRecord(raw, "agents");
|
||||
const list = getAgentsList(agents);
|
||||
const defaultId = resolveDefaultAgentIdFromRaw(raw);
|
||||
const entry = ensureAgentEntry(list, defaultId);
|
||||
if (entry.identity === undefined) {
|
||||
entry.identity = identity;
|
||||
changes.push(`Moved identity → agents.list (id "${defaultId}").identity.`);
|
||||
} else {
|
||||
changes.push("Removed identity (agents.list identity already set).");
|
||||
}
|
||||
agents.list = list;
|
||||
raw.agents = agents;
|
||||
delete raw.identity;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,34 +47,6 @@ function isLegacyGatewayBindHostAlias(value: unknown): boolean {
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["whatsapp"],
|
||||
message: "whatsapp config moved to channels.whatsapp (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["telegram"],
|
||||
message: "telegram config moved to channels.telegram (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["discord"],
|
||||
message: "discord config moved to channels.discord (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["slack"],
|
||||
message: "slack config moved to channels.slack (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["signal"],
|
||||
message: "signal config moved to channels.signal (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["imessage"],
|
||||
message: "imessage config moved to channels.imessage (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["msteams"],
|
||||
message: "msteams config moved to channels.msteams (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["session", "threadBindings"],
|
||||
message:
|
||||
@@ -93,110 +65,11 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
"channels.discord.accounts.<id>.threadBindings.ttlHours was renamed to channels.discord.accounts.<id>.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtlInAccounts(value),
|
||||
},
|
||||
{
|
||||
path: ["routing", "allowFrom"],
|
||||
message:
|
||||
"routing.allowFrom was removed; use channels.whatsapp.allowFrom instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "bindings"],
|
||||
message: "routing.bindings was moved; use top-level bindings instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "agents"],
|
||||
message: "routing.agents was moved; use agents.list instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "defaultAgentId"],
|
||||
message:
|
||||
"routing.defaultAgentId was moved; use agents.list[].default instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "agentToAgent"],
|
||||
message:
|
||||
"routing.agentToAgent was moved; use tools.agentToAgent instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "requireMention"],
|
||||
message:
|
||||
'routing.groupChat.requireMention was removed; use channels.whatsapp/telegram/imessage groups defaults (e.g. channels.whatsapp.groups."*".requireMention) instead (auto-migrated on load).',
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "mentionPatterns"],
|
||||
message:
|
||||
"routing.groupChat.mentionPatterns was moved; use agents.list[].groupChat.mentionPatterns or messages.groupChat.mentionPatterns instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "queue"],
|
||||
message: "routing.queue was moved; use messages.queue instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "transcribeAudio"],
|
||||
message:
|
||||
"routing.transcribeAudio was moved; use tools.media.audio.models instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["telegram", "requireMention"],
|
||||
message:
|
||||
'telegram.requireMention was removed; use channels.telegram.groups."*".requireMention instead (auto-migrated on load).',
|
||||
},
|
||||
{
|
||||
path: ["identity"],
|
||||
message: "identity was moved; use agents.list[].identity instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent"],
|
||||
message:
|
||||
"agent.* was moved; use agents.defaults (and tools.* for tool/elevated/exec settings) instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["memorySearch"],
|
||||
message:
|
||||
"top-level memorySearch was moved; use agents.defaults.memorySearch instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["tools", "bash"],
|
||||
message: "tools.bash was removed; use tools.exec instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "model"],
|
||||
message:
|
||||
"agent.model string was replaced by agents.defaults.model.primary/fallbacks and agents.defaults.models (auto-migrated on load).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModel"],
|
||||
message:
|
||||
"agent.imageModel string was replaced by agents.defaults.imageModel.primary/fallbacks (auto-migrated on load).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "allowedModels"],
|
||||
message: "agent.allowedModels was replaced by agents.defaults.models (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelAliases"],
|
||||
message:
|
||||
"agent.modelAliases was replaced by agents.defaults.models.*.alias (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelFallbacks"],
|
||||
message:
|
||||
"agent.modelFallbacks was replaced by agents.defaults.model.fallbacks (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModelFallbacks"],
|
||||
message:
|
||||
"agent.imageModelFallbacks was replaced by agents.defaults.imageModel.fallbacks (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["messages", "tts", "enabled"],
|
||||
message: "messages.tts.enabled was replaced by messages.tts.auto (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["gateway", "token"],
|
||||
message: "gateway.token is ignored; use gateway.auth.token instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["gateway", "bind"],
|
||||
message:
|
||||
|
||||
Reference in New Issue
Block a user