Skip to content

Commit 8d63d46

Browse files
committed
fix(infra): preserve inline option values
1 parent 05ff7d3 commit 8d63d46

9 files changed

Lines changed: 101 additions & 19 deletions

src/cli/daemon-cli/shared.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import { theme } from "../../terminal/theme.js";
33
import {
44
filterContainerGenericHints,
5+
parsePortFromArgs,
56
renderGatewayServiceStartHints,
67
resolveDaemonContainerContext,
78
resolveRuntimeStatusColor,
@@ -20,6 +21,17 @@ describe("resolveRuntimeStatusColor", () => {
2021
});
2122
});
2223

24+
describe("parsePortFromArgs", () => {
25+
it("rejects inline port values with trailing equals-separated text", () => {
26+
expect(parsePortFromArgs(["--port=123=bad"])).toBeNull();
27+
});
28+
29+
it("accepts valid inline and space-separated port values", () => {
30+
expect(parsePortFromArgs(["--port=14720"])).toBe(14_720);
31+
expect(parsePortFromArgs(["--port", "14721"])).toBe(14_721);
32+
});
33+
});
34+
2335
describe("renderGatewayServiceStartHints", () => {
2436
it("resolves daemon container context from either env key", () => {
2537
expect(

src/cli/daemon-cli/shared.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
buildPlatformRuntimeLogHints,
1111
buildPlatformServiceStartHints,
1212
} from "../../daemon/runtime-hints.js";
13+
import { parseInlineOptionToken } from "../../infra/inline-option-token.js";
1314
import { colorize, isRich, theme } from "../../terminal/theme.js";
1415
import { formatCliCommand } from "../command-format.js";
1516
import { parsePort } from "../shared/parse-port.js";
@@ -76,7 +77,8 @@ export function parsePortFromArgs(programArguments: string[] | undefined): numbe
7677
}
7778
}
7879
if (arg?.startsWith("--port=")) {
79-
const parsed = parsePort(arg.split("=", 2)[1]);
80+
const option = parseInlineOptionToken(arg);
81+
const parsed = parsePort(option.hasInlineValue ? option.inlineValue : undefined);
8082
if (parsed) {
8183
return parsed;
8284
}

src/cli/root-option-value.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isValueToken } from "../infra/cli-root-options.js";
2+
import { parseInlineOptionToken } from "../infra/inline-option-token.js";
23

34
export function takeCliRootOptionValue(
45
raw: string,
@@ -7,9 +8,9 @@ export function takeCliRootOptionValue(
78
value: string | null;
89
consumedNext: boolean;
910
} {
10-
if (raw.includes("=")) {
11-
const value = raw.slice(raw.indexOf("=") + 1);
12-
const trimmed = (value ?? "").trim();
11+
const parsed = parseInlineOptionToken(raw);
12+
if (parsed.hasInlineValue) {
13+
const trimmed = (parsed.inlineValue ?? "").trim();
1314
return { value: trimmed || null, consumedNext: false };
1415
}
1516
const consumedNext = isValueToken(next);

src/infra/command-carriers.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { splitShellArgs } from "../utils/shell-argv.js";
22
import { normalizeExecutableToken } from "./exec-wrapper-tokens.js";
3+
import { parseInlineOptionToken } from "./inline-option-token.js";
34

45
export const COMMAND_CARRIER_EXECUTABLES = new Set(["sudo", "doas", "env", "command", "builtin"]);
56

@@ -97,7 +98,7 @@ export function isEnvAssignmentToken(token: string): boolean {
9798
}
9899

99100
function optionName(token: string): string {
100-
return token.split("=", 1)[0] ?? token;
101+
return parseInlineOptionToken(token).name;
101102
}
102103

103104
type ParsedCarrierOption = {
@@ -113,20 +114,21 @@ function parseCarrierOptionToken(
113114
nonExecutingOptions: ReadonlySet<string> = new Set(),
114115
): ParsedCarrierOption[] | null {
115116
if (token.startsWith("--")) {
116-
const name = optionName(token);
117+
const option = parseInlineOptionToken(token);
118+
const name = option.name;
117119
if (
118120
standaloneOptions.has(name) ||
119121
optionsWithValue.has(name) ||
120122
nonExecutingOptions.has(name)
121123
) {
122-
const valueDelimiter = token.indexOf("=");
123-
return [
124-
{
125-
name,
126-
hasInlineValue: valueDelimiter >= 0,
127-
inlineValue: valueDelimiter >= 0 ? token.slice(valueDelimiter + 1) : undefined,
128-
},
129-
];
124+
const parsedOption: ParsedCarrierOption = {
125+
name,
126+
hasInlineValue: option.hasInlineValue,
127+
};
128+
if (option.hasInlineValue) {
129+
parsedOption.inlineValue = option.inlineValue;
130+
}
131+
return [parsedOption];
130132
}
131133
return null;
132134
}

src/infra/dispatch-wrapper-resolution.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
unwrapEnvInvocation,
77
} from "./command-carriers.js";
88
import { normalizeExecutableToken } from "./exec-wrapper-tokens.js";
9+
import { parseInlineOptionToken } from "./inline-option-token.js";
910

1011
export { unwrapEnvInvocation } from "./command-carriers.js";
1112

@@ -149,7 +150,7 @@ function unwrapDashOptionInvocation(
149150
if (!token.startsWith("-") || token === "-") {
150151
return "stop";
151152
}
152-
const [flag] = lower.split("=", 2);
153+
const { name: flag } = parseInlineOptionToken(lower);
153154
return params.onFlag(flag, lower);
154155
},
155156
adjustCommandIndex: params.adjustCommandIndex,
@@ -253,7 +254,7 @@ function timeInvocationWritesOutputFile(argv: string[]): boolean {
253254
return false;
254255
}
255256
const lower = normalizeLowercaseStringOrEmpty(token);
256-
const [flag] = lower.split("=", 2);
257+
const { name: flag } = parseInlineOptionToken(lower);
257258
if (flag === "-o" || flag === "--output") {
258259
return true;
259260
}
@@ -281,7 +282,7 @@ function unwrapScriptInvocation(
281282
if (!lower.startsWith("-") || lower === "-") {
282283
return "stop";
283284
}
284-
const [flag] = token.split("=", 2);
285+
const { name: flag } = parseInlineOptionToken(token);
285286
if (BSD_SCRIPT_OPTIONS_WITH_VALUE.has(flag)) {
286287
return token.includes("=") ? "continue" : "consume-next";
287288
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parseInlineOptionToken } from "./inline-option-token.js";
3+
4+
describe("parseInlineOptionToken", () => {
5+
it("preserves equals signs after the first separator", () => {
6+
expect(parseInlineOptionToken("--config=a=b.json")).toEqual({
7+
name: "--config",
8+
hasInlineValue: true,
9+
inlineValue: "a=b.json",
10+
});
11+
expect(parseInlineOptionToken("--token=abc==")).toEqual({
12+
name: "--token",
13+
hasInlineValue: true,
14+
inlineValue: "abc==",
15+
});
16+
});
17+
18+
it("distinguishes empty inline values from missing separators", () => {
19+
expect(parseInlineOptionToken("--token=")).toEqual({
20+
name: "--token",
21+
hasInlineValue: true,
22+
inlineValue: "",
23+
});
24+
expect(parseInlineOptionToken("--token")).toEqual({
25+
name: "--token",
26+
hasInlineValue: false,
27+
});
28+
});
29+
});

src/infra/inline-option-token.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export type InlineOptionToken =
2+
| {
3+
name: string;
4+
hasInlineValue: false;
5+
}
6+
| {
7+
name: string;
8+
hasInlineValue: true;
9+
inlineValue: string;
10+
};
11+
12+
export function parseInlineOptionToken(token: string): InlineOptionToken {
13+
const separatorIndex = token.indexOf("=");
14+
if (separatorIndex < 0) {
15+
return { name: token, hasInlineValue: false };
16+
}
17+
return {
18+
name: token.slice(0, separatorIndex),
19+
hasInlineValue: true,
20+
inlineValue: token.slice(separatorIndex + 1),
21+
};
22+
}

src/node-host/invoke-system-run-plan.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,16 @@ const unsafeRuntimeInvocationCases: UnsafeRuntimeInvocationCase[] = [
334334
fs.writeFileSync(path.join(tmp, "preload.mjs"), 'console.log("SAFE")\n');
335335
},
336336
},
337+
{
338+
name: "rejects node inline import values that contain equals signs",
339+
binName: "node",
340+
tmpPrefix: "openclaw-node-import-inline-equals-",
341+
command: ["node", "--import=./pre=load.mjs", "./main.mjs"],
342+
setup: (tmp) => {
343+
fs.writeFileSync(path.join(tmp, "main.mjs"), 'console.log("SAFE")\n');
344+
fs.writeFileSync(path.join(tmp, "pre=load.mjs"), 'console.log("SAFE")\n');
345+
},
346+
},
337347
{
338348
name: "rejects ruby require preloads that approval cannot bind completely",
339349
binName: "ruby",

src/node-host/invoke-system-run-plan.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
unwrapKnownShellMultiplexerInvocation,
1616
} from "../infra/exec-wrapper-resolution.js";
1717
import { sameFileIdentity } from "../infra/fs-safe-advanced.js";
18+
import { parseInlineOptionToken } from "../infra/inline-option-token.js";
1819
import {
1920
advancePosixInlineOptionScan,
2021
POSIX_INLINE_COMMAND_FLAGS,
@@ -146,7 +147,7 @@ const RUBY_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-r", "--require"]);
146147
const PERL_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-M", "-m"]);
147148

148149
function normalizeOptionFlag(token: string): string {
149-
return normalizeLowercaseStringOrEmpty(token.split("=", 1)[0]);
150+
return normalizeLowercaseStringOrEmpty(parseInlineOptionToken(token).name);
150151
}
151152

152153
function readTrimmedArgToken(argv: readonly string[], index: number): string {
@@ -725,7 +726,9 @@ function collectExistingFileOperandIndexes(params: {
725726
return { hits: [], sawOptionValueFile: false };
726727
}
727728
if (token.startsWith("-")) {
728-
const [flag, inlineValue] = token.split("=", 2);
729+
const option = parseInlineOptionToken(token);
730+
const flag = option.name;
731+
const inlineValue = option.hasInlineValue ? option.inlineValue : undefined;
729732
if (params.optionsWithFileValue?.has(normalizeLowercaseStringOrEmpty(flag))) {
730733
if (inlineValue && resolvesToExistingFileSync(inlineValue, params.cwd)) {
731734
hits.push(i);

0 commit comments

Comments
 (0)