harden Cairn desk launch (GHCP review round 5)

Sync signals-dashboard/extension.mjs to the-workshop@91c09d7.

- Render the desk open button unconditionally.

- Drop the deskPath double-quote guard (path is quoted/cwd/single-quoted per platform; guard rejected valid POSIX paths).

- Escape CR/LF in AppleScript string literals.

- On Windows run the agent through cmd.exe /k so PATHEXT resolves .cmd shims (copilot/agency).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
This commit is contained in:
jennyf19
2026-07-20 11:55:23 -07:00
parent e43d6ea666
commit 563148b5b4
+28 -18
View File
@@ -141,9 +141,15 @@ function shSingleQuote(s) {
return "'" + String(s).replace(/'/g, "'\\''") + "'"; return "'" + String(s).replace(/'/g, "'\\''") + "'";
} }
// AppleScript string literal: escape backslashes and double quotes. // AppleScript string literal: escape backslashes, double quotes, and line breaks
// (a raw CR/LF in a path would otherwise terminate the literal and fail to
// compile, while osascript still spawns and trySpawn would report success).
function osaStringLiteral(s) { function osaStringLiteral(s) {
return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; return '"' + String(s)
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\r/g, "\\r")
.replace(/\n/g, "\\n") + '"';
} }
// Resolve symlinks on both sides and confirm the target is the workshop root // Resolve symlinks on both sides and confirm the target is the workshop root
@@ -160,25 +166,29 @@ function isInsideRoot(root, target) {
} }
async function launchDeskConsole(deskPath, deskName, workshopDir) { async function launchDeskConsole(deskPath, deskName, workshopDir) {
// deskPath can't contain a double quote (a real Windows path never does and // deskName must be a plain slug so it is safe on every command line and shell
// it would break out of a quoted argument). deskName must be a plain slug so // below, and the resolved desk must still live inside the workshop root
// it is safe on every command line and shell below, and the resolved desk // (which defeats a symlinked desk that escapes the repo). deskPath itself is
// must still live inside the workshop root (which defeats a symlinked desk // only ever quoted (macOS), passed via -d/cwd (Windows), or handed to the
// that escapes the repo). // shell as a single-quoted literal (Linux), so an empty-path guard is all
if (!deskPath || deskPath.includes('"')) return false; // that is needed — a quote in the path can't break out of any of those.
if (!deskPath) return false;
if (!isSafeDeskNameForLaunch(deskName)) return false; if (!isSafeDeskNameForLaunch(deskName)) return false;
if (!isInsideRoot(workshopDir, deskPath)) return false; if (!isInsideRoot(workshopDir, deskPath)) return false;
const run = [...deskAgentArgv(deskName), "-i", deskOrientPrompt(deskName)]; const run = [...deskAgentArgv(deskName), "-i", deskOrientPrompt(deskName)];
if (process.platform === "win32") { if (process.platform === "win32") {
// Windows Terminal is a GUI app, so it always surfaces its own visible // Run the agent through cmd.exe (/k) so PATHEXT is applied: globally
// window even though the extension host is windowless. It is spawned via // installed CLIs like `copilot`/`agency` are usually .cmd shims that
// argv (no shell), so the contents of run are passed literally. // Windows Terminal or a bare CreateProcess would fail to launch (they
if (await trySpawn("wt.exe", ["-d", deskPath, ...run])) return true; // expect a literal executable, not a PATHEXT name). Windows Terminal is a
// Fallback when wt.exe is absent: a fresh console window via `start`. // GUI app, so it still surfaces its own window from the windowless host.
// `start` re-parses its tail through cmd, so this path is only safe // Each element of run is its own argv token — deskName is a slug and the
// because deskName is a slug and the orientation prompt carries no shell // orientation prompt has no cmd metacharacters — and the desk path is
// metacharacters or quotes; nothing untrusted reaches the parser. // passed via -d/cwd, so nothing untrusted is reparsed by a shell.
return await trySpawn("cmd.exe", ["/c", "start", "", ...run], { cwd: deskPath }); if (await trySpawn("wt.exe", ["-d", deskPath, "cmd", "/k", ...run])) return true;
// Fallback when wt.exe is absent: a fresh console window via `start`,
// still through cmd /k for the same PATHEXT resolution.
return await trySpawn("cmd.exe", ["/c", "start", "", "cmd", "/k", ...run], { cwd: deskPath });
} }
if (process.platform === "darwin") { if (process.platform === "darwin") {
// macOS: `open` can't inject a command, so drive Terminal via AppleScript // macOS: `open` can't inject a command, so drive Terminal via AppleScript
@@ -594,7 +604,7 @@ function renderSignalCard(sig) {
const openBtnStyle = isEscalation const openBtnStyle = isEscalation
? "background:#7f1d1d;border:1px solid #dc2626;color:#fca5a5;padding:2px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;transition:all .15s;" ? "background:#7f1d1d;border:1px solid #dc2626;color:#fca5a5;padding:2px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;transition:all .15s;"
: "background:none;border:1px solid #1e3a5f;color:#7dd3fc;padding:2px 8px;border-radius:4px;font-size:11px;cursor:pointer;transition:all .15s;"; : "background:none;border:1px solid #1e3a5f;color:#7dd3fc;padding:2px 8px;border-radius:4px;font-size:11px;cursor:pointer;transition:all .15s;";
const openBtn = noSignal ? "" : `<button data-act="open" data-desk="${esc(sig.deskName)}" const openBtn = `<button data-act="open" data-desk="${esc(sig.deskName)}"
style="${openBtnStyle}" style="${openBtnStyle}"
onmouseover="this.style.background='#1e3a5f'" onmouseover="this.style.background='#1e3a5f'"
onmouseout="this.style.background='${isEscalation ? '#7f1d1d' : 'transparent'}'" onmouseout="this.style.background='${isEscalation ? '#7f1d1d' : 'transparent'}'"