Skip to content
41 changes: 39 additions & 2 deletions server/container-computer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,43 @@ describe("wakeContainerComputer", () => {
});
});

describe("shellWord", () => {
it("leaves bare shell tokens alone", () => {
expect(shellWord("docker")).toBe("docker");
expect(shellWord("type=bind,source=/srv/x,target=/guest")).toBe(
"type=bind,source=/srv/x,target=/guest",
);
});

it("quotes an empty word so it cannot vanish from the pasted command", () => {
expect(shellWord("", "linux")).toBe("''");
expect(shellWord("", "win32")).toBe('""');
});

it("quotes bracket globs and cmd.exe percent and caret", () => {
expect(shellWord("/srv/[a]", "linux")).toBe(`'/srv/[a]'`);
expect(shellWord("/srv/%PATH%", "win32")).toBe('"/srv/%PATH%"');
expect(shellWord("a^b", "win32")).toBe('"a^b"');
});

it("leaves a backslash or tilde bare, as Windows paths have always printed", () => {
expect(shellWord("C:\\Users\\jay", "linux")).toBe("C:\\Users\\jay");
expect(shellWord("~~workspace", "linux")).toBe("~~workspace");
expect(shellWord("C:\\Users\\my dir", "win32")).toBe(`"C:\\Users\\my dir"`);
// A trailing backslash must not escape the closing quote.
expect(shellWord("C:\\Program Files\\", "win32")).toBe(`"C:\\Program Files\\\\"`);
});

it("single-quotes POSIX words with spaces and escapes single quotes", () => {
expect(shellWord('/srv/my dir', 'linux')).toBe(`'/srv/my dir'`);
expect(shellWord("/srv/o'brien", 'linux')).toBe(`'/srv/o'\\''brien'`);
});

it("double-quotes cmd.exe words because cmd ignores single quotes", () => {
expect(shellWord('/srv/my dir', 'win32')).toBe('"/srv/my dir"');
expect(shellWord('/srv/o"brien', 'win32')).toBe('"/srv/o""brien"');
});
});
describe("setupCommands", () => {
it("derives opaque, distinct per-bot container and workspace identities", () => {
const a = perBotLocalVmTarget("bot-a");
Expand Down Expand Up @@ -1451,14 +1488,14 @@ describe("setupCommands", () => {
expect(command).toContain(`--label ${WORKSPACE_LABEL}=1`);
expect(command).toContain(`--hostname ${CONTAINER}`);
expect(command).toContain(
`--mount ${shellWord(`type=bind,source=${VM_WORKSPACE_DIR},target=${VM_WORKSPACE_GUEST}`)}`,
`--mount type=bind,source=${VM_WORKSPACE_DIR},target=${VM_WORKSPACE_GUEST}`,
);
});

it("asks rootless Podman to map and privately relabel the durable workspace", () => {
const command = setupCommands("podman", "linux").run!;
expect(command).toContain(
`--mount ${shellWord(`type=bind,source=${VM_WORKSPACE_DIR},target=${VM_WORKSPACE_GUEST},relabel=private,U=true`)}`,
`--mount type=bind,source=${VM_WORKSPACE_DIR},target=${VM_WORKSPACE_GUEST},relabel=private,U=true`,
);
});

Expand Down
28 changes: 20 additions & 8 deletions server/container-computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1995,13 +1995,25 @@ export function containerComputerMcp(
};
}

/** Display text a person may paste into a shell. Any word that is not a bare
* shell token (letters, digits, and a small punctuation set) is single-quoted
* so whitespace, `!`, or a Windows path separator cannot be split or
* history-expanded when the string is pasted into cmd or bash. */
export function shellWord(word: string): string {
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(word)
? word
/** Display text a person may paste into a shell. A word carrying whitespace
* or a shell metacharacter (the git credential helper, `!gh auth git-credential`)
* is quoted rather than left to be split or history-expanded. A backslash or
* `~` alone is left bare: those are ordinary in a Windows path, which has
* always been printed as-is here. POSIX shells get single quotes with the `'\\''`
* escape; cmd.exe ignores single quotes, so win32 gets the double-quote form
* with `""` doubling. */
export function shellWord(word: string, platform: NodeJS.Platform = process.platform): string {
if (word === "") return platform === "win32" ? '""' : "''";
// Bracket pairs glob in POSIX shells; cmd.exe additionally expands %VAR%
// inside double quotes and treats ^ as its escape character.
const trigger = platform === "win32"
? /[\s!"'`$&;|<>()*?{}#[\]^%]/
: /[\s!"'`$&;|<>()*?{}#[\]]/;
if (!trigger.test(word)) return word;
// A trailing backslash run would escape the closing quote under the
// Windows argv parser, so the run is doubled before the quote is added.
return platform === "win32"
? `"${word.replace(/"/g, '""').replace(/(\\+)$/, "$1$1")}"`
: `'${word.replace(/'/g, `'\\''`)}'`;
Comment thread
swu-gb-deployer[bot] marked this conversation as resolved.
Resolved
}

Expand Down Expand Up @@ -2041,7 +2053,7 @@ export function setupCommands(
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
};
}
const command = (args: string[]) => [runtime, ...args].map(shellWord).join(" ");
const command = (args: string[]) => [runtime, ...args].map((arg) => shellWord(arg, platform)).join(" ");
return {
install,
runtimeStart,
Expand Down
Loading