Config: fail closed invalid config loads (#39071)
* Config: fail closed invalid config loads * CLI: keep diagnostics on explicit best-effort config * Tests: cover invalid config best-effort diagnostics * Changelog: note invalid config fail-closed fix * Status: pass best-effort config through status-all gateway RPCs * CLI: pass config through gateway secret RPC * CLI: skip plugin loading from invalid config * Tests: align daemon token drift env precedence
This commit is contained in:
@@ -20,4 +20,27 @@ describe("resolveGatewayTokenForDriftCheck", () => {
|
||||
|
||||
expect(token).toBe("config-token");
|
||||
});
|
||||
|
||||
it("does not fall back to caller env for unresolved config token refs", () => {
|
||||
expect(() =>
|
||||
resolveGatewayTokenForDriftCheck({
|
||||
cfg: {
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
token: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toThrow(/gateway\.auth\.token/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,10 @@ export function resolveGatewayTokenForDriftCheck(params: {
|
||||
}) {
|
||||
return resolveGatewayCredentialsFromConfig({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
modeOverride: "local",
|
||||
// Drift checks should compare the persisted gateway token against the
|
||||
// service token, not let an exported shell env mask config drift.
|
||||
// Drift checks should compare the configured local token source against the
|
||||
// persisted service token, not let exported shell env hide stale service state.
|
||||
localTokenPrecedence: "config-first",
|
||||
}).token;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ const service = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: loadConfigMock,
|
||||
readBestEffortConfig: loadConfigMock,
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
resolveGatewayPort: resolveGatewayPortMock,
|
||||
writeConfigFile: writeConfigFileMock,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
isGatewayDaemonRuntime,
|
||||
} from "../../commands/daemon-runtime.js";
|
||||
import { resolveGatewayInstallToken } from "../../commands/gateway-install-token.js";
|
||||
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { isNonFatalSystemdInstallProbeError } from "../../daemon/systemd.js";
|
||||
@@ -27,7 +27,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const cfg = await readBestEffortConfig();
|
||||
const portOverride = parsePort(opts.port);
|
||||
if (opts.port !== undefined && portOverride === null) {
|
||||
fail("Invalid port");
|
||||
|
||||
@@ -32,6 +32,7 @@ const service = {
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
readBestEffortConfig: async () => loadConfig(),
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
@@ -87,7 +88,7 @@ describe("runServiceRestart token drift", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses gateway.auth.token when checking drift", async () => {
|
||||
it("compares restart drift against config token even when caller env is set", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
auth: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Writable } from "node:stream";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { readBestEffortConfig } from "../../config/config.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { checkTokenDrift } from "../../daemon/service-audit.js";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
|
||||
import { isGatewaySecretRefUnavailableError } from "../../gateway/credentials.js";
|
||||
import {
|
||||
isGatewaySecretRefUnavailableError,
|
||||
} from "../../gateway/credentials.js";
|
||||
import { isWSL } from "../../infra/wsl.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveGatewayTokenForDriftCheck } from "./gateway-token-drift.js";
|
||||
@@ -281,7 +283,7 @@ export async function runServiceRestart(params: {
|
||||
try {
|
||||
const command = await params.service.readCommand(process.env);
|
||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN;
|
||||
const cfg = loadConfig();
|
||||
const cfg = await readBestEffortConfig();
|
||||
const configToken = resolveGatewayTokenForDriftCheck({ cfg, env: process.env });
|
||||
const driftIssue = checkTokenDrift({ serviceToken, configToken });
|
||||
if (driftIssue) {
|
||||
|
||||
@@ -33,6 +33,7 @@ const loadConfig = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
readBestEffortConfig: async () => loadConfig(),
|
||||
resolveGatewayPort,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
@@ -32,7 +32,7 @@ async function resolveGatewayRestartPort() {
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
const portFromArgs = parsePortFromArgs(command?.programArguments);
|
||||
return portFromArgs ?? resolveGatewayPort(loadConfig(), mergedEnv);
|
||||
return portFromArgs ?? resolveGatewayPort(await readBestEffortConfig(), mergedEnv);
|
||||
}
|
||||
|
||||
export async function runDaemonUninstall(opts: DaemonLifecycleOptions = {}) {
|
||||
@@ -70,8 +70,8 @@ export async function runDaemonStop(opts: DaemonLifecycleOptions = {}) {
|
||||
export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promise<boolean> {
|
||||
const json = Boolean(opts.json);
|
||||
const service = resolveGatewayService();
|
||||
const restartPort = await resolveGatewayRestartPort().catch(() =>
|
||||
resolveGatewayPort(loadConfig(), process.env),
|
||||
const restartPort = await resolveGatewayRestartPort().catch(async () =>
|
||||
resolveGatewayPort(await readBestEffortConfig(), process.env),
|
||||
);
|
||||
const restartWaitMs = POST_RESTART_HEALTH_ATTEMPTS * POST_RESTART_HEALTH_DELAY_MS;
|
||||
const restartWaitSeconds = Math.round(restartWaitMs / 1000);
|
||||
|
||||
Reference in New Issue
Block a user