Merge 546491bbe13e262dadbefb848aabeb1d8018a69a into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Protocol Zero 2026-03-21 05:00:31 +03:00 committed by GitHub
commit df918f4d40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 1 deletions

View File

@ -12196,7 +12196,7 @@
"filename": "src/config/io.write-config.test.ts",
"hashed_secret": "13951588fd3325e25ed1e3b116d7009fb221c85e",
"is_verified": false,
"line_number": 289
"line_number": 290
}
],
"src/config/model-alias-defaults.test.ts": [

View File

@ -47,6 +47,7 @@ import { normalizeExecSafeBinProfilesInConfig } from "./normalize-exec-safe-bin.
import { normalizeConfigPaths } from "./normalize-paths.js";
import { resolveConfigPath, resolveDefaultConfigCandidates, resolveStateDir } from "./paths.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
import { REDACTED_SENTINEL } from "./redact-snapshot.js";
import { applyConfigOverrides } from "./runtime-overrides.js";
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
import {
@ -1188,6 +1189,20 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
// explicitly set values. Runtime defaults are applied when loading (issue #6070).
const stampedOutputConfig = stampConfigVersion(outputConfig);
const json = JSON.stringify(stampedOutputConfig, null, 2).trimEnd().concat("\n");
if (json.includes(REDACTED_SENTINEL)) {
const sentinel_err = new Error(
`Refusing to write config for "${configPath}": found redaction sentinel ` +
`"${REDACTED_SENTINEL}". This is a bug in the calling code shown in the attached stacktrace — credentials would be permanently lost. ` +
`The config file on disk was not changed.`,
);
deps.logger.error(
`Config write blocked for "${configPath}": redaction sentinel "${REDACTED_SENTINEL}" ` +
`found in output — writing would destroy credentials. Config file was NOT modified.\n${sentinel_err.stack}`,
);
throw sentinel_err;
}
const nextHash = hashConfigRaw(json);
const previousHash = resolveConfigSnapshotHash(snapshot);
const changedPathCount = changedPaths?.size;

View File

@ -3,6 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { createConfigIO } from "./io.js";
import { REDACTED_SENTINEL } from "./redact-snapshot.js";
import type { OpenClawConfig } from "./types.js";
describe("config io write", () => {
@ -548,4 +549,35 @@ describe("config io write", () => {
expect(last.watchCommand).toBe("gateway --force");
});
});
it("refuses to write config containing redaction sentinel (issue #18102)", async () => {
await withSuiteHome(async (home) => {
const initialConfig = {
gateway: { port: 18789 },
channels: { telegram: { botToken: "real-bot-token-123" } },
};
const errorFn = vi.fn();
const { configPath, io, snapshot } = await writeConfigAndCreateIo({
home,
initialConfig,
logger: { warn: vi.fn(), error: errorFn },
});
const poisoned = structuredClone(snapshot.config);
(poisoned as Record<string, unknown>).channels = {
...poisoned.channels,
telegram: {
...poisoned.channels?.telegram,
botToken: REDACTED_SENTINEL,
},
};
await expect(io.writeConfigFile(poisoned)).rejects.toThrow(/redaction sentinel/i);
const ondisk = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
expect(JSON.stringify(ondisk)).not.toContain(REDACTED_SENTINEL);
expect(ondisk).toEqual(initialConfig);
expect(errorFn).toHaveBeenCalled();
});
});
});