CLI: harden profile passthrough + dev reset interlock
This commit is contained in:
parent
6fc6fd0add
commit
1e1aa9cffc
@ -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");
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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=")))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user