Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions .github/extensions/signals-dashboard/extension.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";

const servers = new Map();
Expand Down Expand Up @@ -210,6 +212,20 @@ function capturePluginMcpJson(workshopDir, agent) {
? ["copilot", "--no-default-mcps",
"plugins", "list", "--kind", "mcp", "--scope", "plugin", "--json"]
: ["plugins", "list", "--kind", "mcp", "--scope", "plugin", "--json"];
const shim = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
if (shim && !isSafeWindowsCmdShim(command)) {
resolve(null);
return;
}
const spawnCommand = shim ? resolveSystem32Executable("cmd.exe") : command;
if (!spawnCommand) {
resolve(null);
return;
}
const spawnArgs = shim
? ["/d", "/s", "/c",
`"${[command, ...args].map(quoteWindowsCmdArgument).join(" ")}"`]
: args;

let settled = false;
let stdout = "";
Expand All @@ -226,10 +242,11 @@ function capturePluginMcpJson(workshopDir, agent) {

let child;
try {
child = spawn(command, args, {
child = spawn(spawnCommand, spawnArgs, {
cwd: workshopDir,
detached: process.platform !== "win32",
windowsHide: true,
windowsVerbatimArguments: shim,
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
Expand Down Expand Up @@ -796,10 +813,10 @@ function renderSignalCard(sig) {
title="Open this desk with the ${esc(DEFAULT_DESK_PROFILE)} tool profile">open</button>`;
const connectedBtn = DEFAULT_DESK_PROFILE === "connected" ? "" : `
<button data-act="open" data-profile="connected" data-desk="${esc(sig.deskName)}"
style="background:none;border:1px solid #262626;color:#64748b;padding:2px 7px;border-radius:4px;
style="background:none;border:1px solid #262626;color:#94a3b8;padding:2px 7px;border-radius:4px;
font-size:10px;cursor:pointer;transition:all .15s;"
onmouseover="this.style.borderColor='#475569';this.style.color='#cbd5e1'"
onmouseout="this.style.borderColor='#262626';this.style.color='#64748b'"
onmouseout="this.style.borderColor='#262626';this.style.color='#94a3b8'"
title="Open with every configured MCP and tool">connected</button>`;

let escalationBlock = "";
Expand Down
8 changes: 8 additions & 0 deletions .github/extensions/signals-dashboard/launch-profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export function isWindowsAppExecutionAlias(candidate, localAppData) {
return normalized.startsWith(root.toLowerCase()) && normalized.endsWith(".exe");
}

export function quoteWindowsCmdArgument(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}

export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value);
}

export function parsePluginMcpNames(text) {
let parsed;
try { parsed = JSON.parse(text); }
Expand Down
38 changes: 38 additions & 0 deletions .github/extensions/signals-dashboard/launch-profile.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";

test("normalizes supported profiles and defaults unknown values to repo", () => {
Expand All @@ -28,6 +34,38 @@ test("recognizes Windows App Execution Alias paths without trusting repository e
"C:\\Users\\person\\AppData\\Local"), false);
});

test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () => {
assert.equal(
quoteWindowsCmdArgument("C:\\Program Files\\Agency\\agency.cmd"),
"\"C:\\Program Files\\Agency\\agency.cmd\"");
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
});

test("executes a Windows cmd shim with safe quoting", {
skip: process.platform !== "win32",
}, () => {
const root = mkdtempSync(join(tmpdir(), "workshop-profile-"));
const shimDir = join(root, "Shim Name");
mkdirSync(shimDir);
const shim = join(shimDir, "copilot.cmd");
writeFileSync(shim, "@echo off\r\necho {\"plugins\":[]}\r\n");

const cmd = join(process.env.SystemRoot, "System32", "cmd.exe");
const commandLine = `"${[shim, "plugins", "list"]
.map(quoteWindowsCmdArgument)
.join(" ")}"`;
const result = spawnSync(cmd, ["/d", "/s", "/c", commandLine], {
encoding: "utf8",
windowsHide: true,
windowsVerbatimArguments: true,
});

assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\{"plugins":\[\]\}/);
});

test("extracts enabled plugin-scoped MCP names and rejects unsafe names", () => {
const names = parsePluginMcpNames(JSON.stringify({
plugins: [
Expand Down
3 changes: 3 additions & 0 deletions docs/github-copilot-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ Each desk card has two launch choices:
- **connected** — the full configured tool surface for work that needs external
systems such as issue trackers, mail, or service APIs.

Set `WORKSHOP_DESK_PROFILE=connected` to make the main **open** button use the
full tool surface; in that mode the separate **connected** button is omitted.

Both launch a CLI right in that desk's folder: an in-place session inside your
workshop repo, so every desk stays in the one repo you coordinate through (its
journal, its `.signals`, and the shared board) rather than a separate checkout
Expand Down
3 changes: 3 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ remains available for cross-desk reads, while ambient plugin MCPs stay out of th
topic desk's prompt. Choose **connected** when the desk needs every configured
external system. Agency remains the launcher when installed.

Set `WORKSHOP_DESK_PROFILE=connected` to retain the full surface on the main
**open** button; the separate **connected** button is then omitted.

## What you get

- **Desks** — long-running agents, each with its own memory and history.
Expand Down
Loading