fix: allow doctor repair size drops

This commit is contained in:
Peter Steinberger
2026-05-01 13:02:22 +01:00
parent 2fbe808a32
commit 32359e667b
3 changed files with 79 additions and 3 deletions

View File

@@ -215,6 +215,11 @@ export type ConfigWriteOptions = {
* Normal writers must keep this false so clobbers are rejected before disk commit.
*/
allowDestructiveWrite?: boolean;
/**
* Allow an intentional large config size drop while keeping other destructive
* guards active. Used by repair flows that remove stale or legacy config.
*/
allowConfigSizeDrop?: boolean;
/**
* Suppress human-readable output logs (overwrite/anomaly messages).
* Useful when the caller wants machine-readable output only (--json mode).
@@ -399,9 +404,14 @@ function resolveConfigWriteSuspiciousReasons(params: {
return reasons;
}
function resolveConfigWriteBlockingReasons(suspicious: string[]): string[] {
function resolveConfigWriteBlockingReasons(
suspicious: string[],
options: Pick<ConfigWriteOptions, "allowConfigSizeDrop"> = {},
): string[] {
return suspicious.filter(
(reason) => reason.startsWith("size-drop:") || reason === "gateway-mode-removed",
(reason) =>
(reason.startsWith("size-drop:") && options.allowConfigSizeDrop !== true) ||
reason === "gateway-mode-removed",
);
}
@@ -2165,7 +2175,7 @@ export function createConfigIO(
}),
});
};
const blockingReasons = resolveConfigWriteBlockingReasons(suspiciousReasons);
const blockingReasons = resolveConfigWriteBlockingReasons(suspiciousReasons, options);
if (blockingReasons.length > 0 && options.allowDestructiveWrite !== true) {
const rejectedPath = `${configPath}.rejected.${formatConfigArtifactTimestamp(new Date().toISOString())}`;
await deps.fs.promises
@@ -2426,6 +2436,7 @@ export async function writeConfigFile(
}),
unsetPaths: resolveManagedUnsetPathsForWrite(options.unsetPaths),
allowDestructiveWrite: options.allowDestructiveWrite,
allowConfigSizeDrop: options.allowConfigSizeDrop,
skipRuntimeSnapshotRefresh: options.skipRuntimeSnapshotRefresh,
skipOutputLogs: options.skipOutputLogs,
});

View File

@@ -602,6 +602,68 @@ describe("config io write", () => {
});
});
it("allows intentional size-drop writes without disabling gateway-mode protection", async () => {
await withSuiteHome(async (home) => {
const configPath = path.join(home, ".openclaw", "openclaw.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
const original = {
meta: { lastTouchedVersion: "2026.4.30" },
gateway: { mode: "local" },
channels: {
telegram: {
enabled: true,
allowFrom: Array.from({ length: 80 }, (_, index) => `telegram:${index}`),
},
},
} satisfies ConfigFileSnapshot["config"];
const originalRaw = `${JSON.stringify(original, null, 2)}\n`;
await fs.writeFile(configPath, originalRaw, "utf-8");
const io = createConfigIO({
env: { VITEST: "true" } as NodeJS.ProcessEnv,
homedir: () => home,
logger: silentLogger,
});
const baseSnapshot = {
path: configPath,
exists: true,
raw: originalRaw,
parsed: original,
sourceConfig: original,
resolved: original,
valid: true,
runtimeConfig: original,
config: original,
issues: [],
warnings: [],
legacyIssues: [],
} satisfies ConfigFileSnapshot;
await expect(
io.writeConfigFile(
{ meta: original.meta, gateway: { mode: "local" } },
{
allowConfigSizeDrop: true,
baseSnapshot,
},
),
).resolves.toMatchObject({
persistedConfig: expect.objectContaining({ gateway: { mode: "local" } }),
});
await expect(
io.writeConfigFile(
{ meta: original.meta },
{
allowConfigSizeDrop: true,
baseSnapshot,
},
),
).rejects.toMatchObject({
code: "CONFIG_WRITE_REJECTED",
});
});
});
it("keeps authored agent provider params during narrowed internal agent writes", async () => {
await withSuiteHome(async (home) => {
const configPath = path.join(home, ".openclaw", "openclaw.json");

View File

@@ -526,6 +526,9 @@ async function runWriteConfigHealth(ctx: DoctorHealthFlowContext): Promise<void>
await replaceConfigFile({
nextConfig: ctx.cfg,
afterWrite: { mode: "auto" },
writeOptions: {
allowConfigSizeDrop: ctx.configResult.shouldWriteConfig === true,
},
});
logConfigUpdated(ctx.runtime);
const backupPath = `${CONFIG_PATH}.bak`;