Skip to content
25 changes: 25 additions & 0 deletions server/container-computer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
perBotLocalVmTarget,
podmanSecurityIsHardened,
setupCommands,
shellWord,
workspaceSources,
type CommandRunner,
type LocalVmTarget,
Expand Down Expand Up @@ -1393,6 +1394,30 @@ 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("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"`);
});

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
23 changes: 15 additions & 8 deletions server/container-computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,20 @@ export function containerComputerMcp(
};
}

/** 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 (!/[\s!"'`$&;|<>()*?{}#]/.test(word)) return word;
Comment thread
jaywedgeworth22 marked this conversation as resolved.
Resolved
Comment thread
jaywedgeworth22 marked this conversation as resolved.
Resolved
return platform === "win32"
? `"${word.replace(/"/g, '""')}"`
Comment thread
jaywedgeworth22 marked this conversation as resolved.
Resolved
: `'${word.replace(/'/g, `'\\''`)}'`;
Comment thread
swu-gb-deployer[bot] marked this conversation as resolved.
Resolved
}

/** Commands shown as a transparent fallback. Normal setup builds the pinned
* derivative through the API, so users do not need to author a Dockerfile. */
export function setupCommands(
Expand Down Expand Up @@ -2031,14 +2045,7 @@ export function setupCommands(
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
};
}
// Display text a person may paste into a shell, so an argument with whitespace
// or a shell metacharacter (the git credential helper, `!gh auth git-credential`)
// is single-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.
const shellWord = (word: string) =>
/[\s!"'`$&;|<>()*?{}#]/.test(word) ? `'${word.replace(/'/g, `'\\''`)}'` : word;
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