Gateway: harden dev reset safety checks
This commit is contained in:
parent
0a30503239
commit
6fc6fd0add
@ -1,3 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { withTempSecretFiles } from "../../test-utils/secret-file-fixture.js";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -362,4 +364,55 @@ describe("gateway run option collisions", () => {
|
||||
|
||||
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
|
||||
});
|
||||
|
||||
it("hard-stops --dev --reset when state/config match non-dev profile defaults", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
vi.stubEnv("OPENCLAW_PROFILE", "work");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", "/Users/test/.openclaw-work");
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", "/Users/test/.openclaw-work/openclaw.json");
|
||||
resolveStateDir.mockReturnValue("/Users/test/.openclaw-work");
|
||||
resolveConfigPath.mockReturnValue("/Users/test/.openclaw-work/openclaw.json");
|
||||
|
||||
await expectGatewayExit(["gateway", "run", "--dev", "--reset"]);
|
||||
|
||||
expect(ensureDevGatewayConfig).not.toHaveBeenCalled();
|
||||
expect(runtimeErrors.join("\n")).toContain(
|
||||
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats symlinked default paths as default reset targets", async () => {
|
||||
const home = "/Users/test";
|
||||
const defaultStateDir = path.join(home, ".openclaw");
|
||||
const defaultConfigPath = path.join(defaultStateDir, "openclaw.json");
|
||||
const aliasStateDir = path.join(home, ".openclaw-alias");
|
||||
const aliasConfigPath = path.join(aliasStateDir, "openclaw.json");
|
||||
const realpathSpy = vi.spyOn(fs, "realpathSync").mockImplementation((candidate) => {
|
||||
const resolved = path.resolve(String(candidate));
|
||||
if (resolved === path.resolve(aliasStateDir)) {
|
||||
return path.resolve(defaultStateDir);
|
||||
}
|
||||
if (resolved === path.resolve(aliasConfigPath)) {
|
||||
return path.resolve(defaultConfigPath);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
|
||||
vi.stubEnv("HOME", home);
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", aliasStateDir);
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", aliasConfigPath);
|
||||
resolveStateDir.mockReturnValue(aliasStateDir);
|
||||
resolveConfigPath.mockReturnValue(aliasConfigPath);
|
||||
|
||||
try {
|
||||
await expectGatewayExit(["gateway", "run", "--dev", "--reset"]);
|
||||
} finally {
|
||||
realpathSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(ensureDevGatewayConfig).not.toHaveBeenCalled();
|
||||
expect(runtimeErrors.join("\n")).toContain(
|
||||
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -183,6 +183,31 @@ function resolveDevResetPaths(env: NodeJS.ProcessEnv = process.env): {
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalizePathForCompare(rawPath: string): string {
|
||||
const resolvedPath = path.resolve(rawPath);
|
||||
try {
|
||||
return fs.realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProfileDefaultPaths(
|
||||
profile: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
} {
|
||||
const home = resolveRequiredHomeDir(env, os.homedir);
|
||||
const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`;
|
||||
const stateDir = path.join(home, `.openclaw${suffix}`);
|
||||
return {
|
||||
stateDir,
|
||||
configPath: path.join(stateDir, "openclaw.json"),
|
||||
};
|
||||
}
|
||||
|
||||
async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
const envProfile = process.env.OPENCLAW_PROFILE?.trim();
|
||||
if (envProfile && !isValidProfileName(envProfile)) {
|
||||
@ -203,16 +228,32 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
|
||||
if (opts.reset && devMode) {
|
||||
const paths = resolveDevResetPaths(process.env);
|
||||
const resolvedStateDir = path.resolve(paths.stateDir);
|
||||
const resolvedConfigPath = path.resolve(paths.configPath);
|
||||
const stateIsDefault = resolvedStateDir === path.resolve(paths.defaultStateDir);
|
||||
const configIsDefault = resolvedConfigPath === path.resolve(paths.defaultConfigPath);
|
||||
const stateMatchesDev = resolvedStateDir === path.resolve(paths.expectedDevStateDir);
|
||||
const configMatchesDev = resolvedConfigPath === path.resolve(paths.expectedDevConfigPath);
|
||||
const resolvedStateDir = canonicalizePathForCompare(paths.stateDir);
|
||||
const resolvedConfigPath = canonicalizePathForCompare(paths.configPath);
|
||||
const stateIsDefault = resolvedStateDir === canonicalizePathForCompare(paths.defaultStateDir);
|
||||
const configIsDefault =
|
||||
resolvedConfigPath === canonicalizePathForCompare(paths.defaultConfigPath);
|
||||
const stateMatchesDev =
|
||||
resolvedStateDir === canonicalizePathForCompare(paths.expectedDevStateDir);
|
||||
const configMatchesDev =
|
||||
resolvedConfigPath === canonicalizePathForCompare(paths.expectedDevConfigPath);
|
||||
const profileDefaultPaths = envProfile
|
||||
? resolveProfileDefaultPaths(envProfile, process.env)
|
||||
: null;
|
||||
const stateMatchesProfileDefault = profileDefaultPaths
|
||||
? resolvedStateDir === canonicalizePathForCompare(profileDefaultPaths.stateDir)
|
||||
: false;
|
||||
const configMatchesProfileDefault = profileDefaultPaths
|
||||
? resolvedConfigPath === canonicalizePathForCompare(profileDefaultPaths.configPath)
|
||||
: false;
|
||||
const targetMatchesProfileDefaults = stateMatchesProfileDefault && configMatchesProfileDefault;
|
||||
const hasStateOverride = Boolean(process.env.OPENCLAW_STATE_DIR?.trim());
|
||||
const hasConfigOverride = Boolean(process.env.OPENCLAW_CONFIG_PATH?.trim());
|
||||
const hasExplicitCustomTarget =
|
||||
(hasStateOverride || hasConfigOverride) && !stateIsDefault && !configIsDefault;
|
||||
(hasStateOverride || hasConfigOverride) &&
|
||||
!stateIsDefault &&
|
||||
!configIsDefault &&
|
||||
!targetMatchesProfileDefaults;
|
||||
|
||||
if (!hasExplicitCustomTarget && (!stateMatchesDev || !configMatchesDev)) {
|
||||
defaultRuntime.error(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user