CLI: harden profile passthrough + dev reset interlock

This commit is contained in:
Tristan Manchester 2026-03-02 21:18:43 +01:00
parent 6fc6fd0add
commit 1e1aa9cffc
4 changed files with 88 additions and 8 deletions

View File

@ -381,6 +381,22 @@ describe("gateway run option collisions", () => {
);
});
it("hard-stops --dev --reset when state is profile-default but config is custom", 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", "/tmp/custom-dev/openclaw.json");
resolveStateDir.mockReturnValue("/Users/test/.openclaw-work");
resolveConfigPath.mockReturnValue("/tmp/custom-dev/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");

View File

@ -246,14 +246,14 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
const configMatchesProfileDefault = profileDefaultPaths
? resolvedConfigPath === canonicalizePathForCompare(profileDefaultPaths.configPath)
: false;
const targetMatchesProfileDefaults = stateMatchesProfileDefault && configMatchesProfileDefault;
const stateTargetsProfileDefault = stateIsDefault || stateMatchesProfileDefault;
const configTargetsProfileDefault = configIsDefault || 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 &&
!targetMatchesProfileDefaults;
hasStateOverride &&
!stateTargetsProfileDefault &&
(!hasConfigOverride || !configTargetsProfileDefault);
if (!hasExplicitCustomTarget && (!stateMatchesDev || !configMatchesDev)) {
defaultRuntime.error(

View File

@ -176,6 +176,41 @@ describe("parseCliProfileArgs", () => {
]);
});
it("consumes root option values before command-path passthrough guard", () => {
const res = parseCliProfileArgs([
"node",
"openclaw",
"--log-level",
"debug",
"nodes",
"run",
"--",
"aws",
"--profile",
"prod",
"sts",
"get-caller-identity",
]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBeNull();
expect(res.argv).toEqual([
"node",
"openclaw",
"--log-level",
"debug",
"nodes",
"run",
"--",
"aws",
"--profile",
"prod",
"sts",
"get-caller-identity",
]);
});
it("parses --profile value and strips it", () => {
const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]);
if (!res.ok) {

View File

@ -28,6 +28,18 @@ function takeValue(
}
const ARBITRARY_ARG_COMMAND_PATHS = [["nodes", "run"], ["docs"]] as const;
const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level"]);
function isRootOptionValueToken(arg: string | undefined): boolean {
if (!arg || arg === "--") {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function shouldGuardTrailingArgsFromProfileParsing(args: string[]): boolean {
const commandTokens: string[] = [];
@ -38,12 +50,17 @@ function shouldGuardTrailingArgsFromProfileParsing(args: string[]): boolean {
break;
}
if (arg === "--dev") {
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
continue;
}
if (arg === "--profile" || arg.startsWith("--profile=")) {
if (arg === "--profile") {
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
continue;
}
if (ROOT_VALUE_FLAGS.has(arg)) {
const next = args[i + 1];
if (isRootOptionValueToken(next)) {
i += 1;
}
continue;
@ -108,6 +125,18 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
continue;
}
if (arg === "--log-level" || arg.startsWith("--log-level=")) {
out.push(arg);
if (arg === "--log-level") {
const next = args[i + 1];
if (isRootOptionValueToken(next)) {
out.push(next);
i += 1;
}
}
continue;
}
if (
sawCommand &&
(guardTrailingArgs || (arg !== "--profile" && !arg.startsWith("--profile=")))