From 3e8976ef581b0bfc746ff2a3daf512f5db625840 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 10:29:42 -0700 Subject: [PATCH 1/9] feat: add fail-closed Local Delegation to Cairn dashboard Port the-workshop Local Delegation seam into signals-dashboard 0.3.0. Orthogonal off/on control beside repo/connected profiles; enable only when local-agent-delegation is installed and a qualified route receipt is present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- .github/plugin/marketplace.json | 4 +- extensions/signals-dashboard/README.md | 139 ++++++++ extensions/signals-dashboard/extension.mjs | 218 +++++++++++-- .../signals-dashboard/local-delegation.mjs | 301 ++++++++++++++++++ .../local-delegation.test.mjs | 190 +++++++++++ extensions/signals-dashboard/package.json | 6 +- plugins/signals-dashboard/plugin.json | 7 +- plugins/the-workshop/README.md | 7 + 8 files changed, 831 insertions(+), 41 deletions(-) create mode 100644 extensions/signals-dashboard/README.md create mode 100644 extensions/signals-dashboard/local-delegation.mjs create mode 100644 extensions/signals-dashboard/local-delegation.test.mjs diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index b5458c93e..fae81b25c 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1282,8 +1282,8 @@ { "name": "signals-dashboard", "source": "plugins/signals-dashboard", - "description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", - "version": "0.2.0" + "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.", + "version": "0.3.0" }, { "name": "site-studio", diff --git a/extensions/signals-dashboard/README.md b/extensions/signals-dashboard/README.md new file mode 100644 index 000000000..0a54169eb --- /dev/null +++ b/extensions/signals-dashboard/README.md @@ -0,0 +1,139 @@ +# 🪨 Cairn — Signals Dashboard + +A live GitHub Copilot CLI **canvas extension** that shows the pulse of every +desk in your Workshop. It reads the agent signals your desks emit and renders +them as a compact, always-current dashboard in a side panel — so you can direct +the work instead of polling each desk by hand. + +It replaces the old Blazor **WorkshopRoom** dashboard (`src/WorkshopRoom/`) with +a native canvas that runs inside GHCP, with no separate web app to launch. + +## What it is + +Each desk in the Workshop leaves signals behind — small stones on the trail — +by writing JSON files into its `.signals/` folder. Cairn scans those folders, +picks the most recent signal per desk, and renders: + +- **Score bars** for the desk's self-assessment (intent, confidence, accuracy, + completeness). +- **Patterns** the desk reported: what worked ✓, what was hard △, and skill + gaps ✗. +- **Escalations** — desks that raised their hand, with what they're blocked on + and their recommendation, pinned to the top and pulsing red. + +## How to open it + +The dashboard is registered as the **🪨 Cairn** canvas (`signals-dashboard`). +Ask Copilot to open it and pass your workshop root as `workshopDir`: + +> Open the 🪨 Cairn canvas with `workshopDir` set to the folder that contains +> my `desks/` directory. + +`workshopDir` must be the **absolute path to the workshop root** — the folder +that holds `desks/` (and optionally `classroom/`). If omitted, it falls back to +the current working directory. + +## Features + +- **Signal scanning** — walks `desks/*/.signals/` and `classroom/*/.signals/`, + reading the newest `*.json` per desk (mirrors `SignalReader.cs`). +- **Score bars** — color-coded intent / confidence / accuracy / completeness, + scored out of 5. +- **Escalation alerts** — escalation signals sort to the very top, render with a + pulsing red border, and surface the blocker + recommendation. +- **Active desks first** — sorted escalations → recent signals → desks with no + signal yet, then by recency. +- **Stash / restore** — pause a workstream by stashing its desk. Stashed desks + drop off the active view and auto-expire after a **48-hour TTL**; restore any + time before then. Stash state lives in `.desk-stash.json` at the workshop + root. +- **Auto-refresh** — the panel refreshes every 5 seconds using a background + fetch (no full page reload), so scores and escalations stay current smoothly. +- **Summary bar** — desk count, how many are reporting vs. awaiting, an + escalation badge, and average scores across the room. +- **Cost-aware desk launch** — **open** starts a repo-profile desk that keeps the + verified Workshop root available while suppressing ambient plugin MCPs. + **connected** preserves every configured MCP for work that needs external + systems. Agency remains the preferred wrapper when installed. +- **Local Delegation** — orthogonal off/on control. When available and on, the + frontier desk may use the installed [`local-agent-delegation`](https://github.com/jennyf19/sealed-delegation) + skill for bounded, independently gated read/evidence work. Fail-closed: no + skill or no qualified route receipt means the toggle cannot take effect, and + no local-savings credit is awarded. + +## Agent actions + +The canvas also exposes actions Copilot can invoke directly: + +- `refresh` — force a rescan and return current signal data as JSON. +- `stash` — stash a desk by `deskName`. +- `restore` — restore a stashed desk by `deskName`. +- `open_desk` — open a desk with optional `profile: "repo" | "connected"` and + optional `localDelegation: "off" | "on"`. + +## Desk launch profiles + +`repo` is the default. At launch, Cairn asks Copilot for the enabled +plugin-scoped MCP inventory and disables those ambient servers for the topic +desk. User-, workspace-, organization-, and built-in resources are left alone. +If discovery fails, Copilot plugin MCP suppression fails open; Agency repo mode +still omits Agency's own default MCPs. + +When Agency is installed, Cairn keeps the existing `agency copilot` launch and +adds Agency's `--no-default-mcps` in repo mode. Outside Agency, the same profile +is applied directly to Copilot CLI. + +Both profiles pass `--add-dir ` so a desk can intentionally read +another desk's journal or artifact without receiving access outside the room. + +Set `WORKSHOP_DESK_PROFILE=connected` to retain the historical default for the +main **open** button. The separate **connected** button is always available when +repo mode is the default. + +## Local Delegation + +Local Delegation is **not** a third desk profile and does not replace the frontier +model. It is a separate permission bit: + +```text +repo / connected = which MCPs and tools the frontier desk can see +Local Delegation off/on = whether the frontier desk may invoke a bounded local worker +``` + +Availability is fail-closed. Cairn enables the lane only when: + +1. the `local-agent-delegation` skill is installed, and +2. a qualified route is declared via `WORKSHOP_LOCAL_DELEGATION_ROUTE_ID` or a + receipt at `~/.copilot/local-agent-runs/qualified-route.json` + (`status: "qualified"`, safe `route_id`). + +Operator preference is stored at the workshop root in `.local-delegation.json`. +When the preference is on but availability fails, opens still launch as frontier +desks and surface the reason — they never silently fall back with savings credit. + +When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched +process and appends a short orientation note that local output is a proposal. +The runtime, launcher, and gates remain owned by +[Sealed Delegation](https://github.com/jennyf19/sealed-delegation). + +## Signal shape + +Cairn reads the agent-signals protocol used across the Workshop: + +```json +{ + "signal_type": "execution", + "agent_name": "desk-name", + "self_assessment": { "intent": 5, "confidence": 4, "accuracy": 4, "completeness": 3 }, + "patterns": { "what_worked": "...", "what_was_hard": "...", "skill_gap": "..." }, + "escalation": { "reason": "...", "blocked_on": "...", "recommendation": "..." } +} +``` + +`escalation` is only present on `signal_type: "escalation"` signals. + +## Replaces the Blazor WorkshopRoom + +This canvas supersedes the standalone Blazor dashboard in `src/WorkshopRoom/`. +The data is the truth and the UI is just a view — Cairn renders the same signal +data natively inside GHCP, so there's no separate server to run. diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index 3309f0f5f..53b0f56cc 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -19,6 +19,16 @@ import { parsePluginMcpNames, quoteWindowsCmdArgument, } from "./launch-profile.mjs"; +import { + LOCAL_DELEGATION_STATE_FILE, + buildLocalDelegationLaunchEnv, + deskOrientPrompt, + normalizeLocalDelegationPreference, + parseLocalDelegationState, + resolveLocalDelegationAvailability, + resolveLocalDelegationLaunch, + serializeLocalDelegationState, +} from "./local-delegation.mjs"; const servers = new Map(); const STASH_TTL_MS = 48 * 60 * 60 * 1000; @@ -60,11 +70,10 @@ function isValidDeskName(name) { // path is then only ever passed as a spawn cwd, an argv element, or a // single-quoted literal inside the macOS Terminal command — never concatenated // raw onto a command line — so no character filtering of the path is required. -function deskOrientPrompt(deskName) { - return `You are sitting down at the ${deskName} desk in this workshop. ` + - `Read journal.md in this folder first to pick up where the last session ` + - `left off, then continue the desk's work. Write your journal before you stop.`; -} +// +// Local Delegation is orthogonal to repo/connected: it never changes the tool +// surface. When effective, only the orientation prompt and child env mark that +// the frontier desk may use the installed local-agent-delegation skill. // Spawn detached and resolve true only once the OS confirms the process // started ('spawn'), false on failure ('error', e.g. the binary is missing) so @@ -354,7 +363,37 @@ function isInsideRoot(root, target) { } catch { return false; } } -async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) { +async function readLocalDelegationPreference(workshopDir) { + try { + const raw = JSON.parse(await readFile(join(workshopDir, LOCAL_DELEGATION_STATE_FILE), "utf8")); + return parseLocalDelegationState(raw).preference; + } catch { + return normalizeLocalDelegationPreference( + process.env.WORKSHOP_LOCAL_DELEGATION_PREFERENCE, "off"); + } +} + +async function writeLocalDelegationPreference(workshopDir, preference) { + const state = serializeLocalDelegationState({ preference }); + await writeFile( + join(workshopDir, LOCAL_DELEGATION_STATE_FILE), + JSON.stringify(state, null, 2) + "\n", + "utf8"); + return state; +} + +function currentLocalDelegationLaunch(preference) { + const availability = resolveLocalDelegationAvailability(); + return resolveLocalDelegationLaunch({ preference, availability }); +} + +async function launchDeskConsole( + deskPath, + deskName, + workshopDir, + profile = DEFAULT_DESK_PROFILE, + localDelegation = { effective: false }, +) { // deskName must be a plain slug so it is safe on every command line and shell // below, and the resolved desk must still live inside the workshop root // (which defeats a symlinked desk that escapes the repo). deskPath itself is @@ -367,23 +406,25 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA if (!isInsideRoot(workshopDir, deskPath)) return false; const agent = await deskAgentArgv(deskName, workshopDir, profile); if (!agent) return false; - const run = [...agent, "-i", deskOrientPrompt(deskName)]; + const effective = Boolean(localDelegation?.effective); + const run = [...agent, "-i", deskOrientPrompt(deskName, { localDelegationEffective: effective })]; + const env = buildLocalDelegationLaunchEnv(process.env, { localDelegationEffective: effective }); if (process.platform === "win32") { const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir }); const cmd = resolveSystem32Executable("cmd.exe"); const direct = /\.(exe|com)$/i.test(run[0]); - if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true; + if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run], { env })) return true; // Older installs can expose .cmd/.bat shims. Only use cmd.exe when every // argument is free of cmd metacharacters; otherwise fail closed and let // the UI copy the desk path rather than reparse an unsafe workshop path. const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)); if (cmdSafe && wt && cmd && - await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true; + await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run], { env })) return true; // Fallback when wt.exe is absent: a fresh console window via `start`, // still through cmd /k only when the arguments are safe for reparsing. return cmdSafe && cmd - ? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath }) + ? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath, env }) : false; } if (process.platform === "darwin") { @@ -393,13 +434,18 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA // to cd into the desk and exec the agent. Each argv element is POSIX // single-quoted so the shell can't reinterpret it, and osascript itself // is spawned via argv (no shell). - const line = "cd " + shSingleQuote(deskPath) + " && exec " + + // Local-delegation env is exported in-line so the Terminal session sees it + // without inheriting a polluted parent shell forever. + const envPrefix = effective + ? "export WORKSHOP_LOCAL_DELEGATION=enabled; " + : "unset WORKSHOP_LOCAL_DELEGATION; "; + const line = "cd " + shSingleQuote(deskPath) + " && " + envPrefix + "exec " + run.map(shSingleQuote).join(" "); const script = 'tell application "Terminal"\n' + " activate\n" + " do script " + osaStringLiteral(line) + "\n" + "end tell"; - return await trySpawn(osascript, ["-e", script]); + return await trySpawn(osascript, ["-e", script], { env }); } // Linux/other: best-effort across common terminal emulators. Each is spawned // via argv (no shell) with the agent command after the emulator's exec flag, @@ -412,7 +458,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA ]; for (const [term, args] of linuxTerms) { const executable = resolveOnPath(term, { excludedRoot: workshopDir }); - if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true; + if (executable && await trySpawn(executable, args, { cwd: deskPath, env })) return true; } return false; } @@ -728,7 +774,38 @@ function avgScore(signals) { return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") }; } -function renderSummaryBar(activeSignals) { +function renderLocalDelegationControl(localDelegation) { + const pref = localDelegation?.preference || "off"; + const available = Boolean(localDelegation?.availability?.available); + const effective = Boolean(localDelegation?.effective); + const reason = localDelegation?.availability?.reason || "Local Delegation unavailable"; + const next = pref === "on" ? "off" : "on"; + const label = effective ? "On" : (pref === "on" ? "On*" : "Off"); + const color = effective ? "#86efac" : (pref === "on" ? "#fbbf24" : "#94a3b8"); + const border = effective ? "#166534" : (pref === "on" ? "#854d0e" : "#334155"); + const title = available + ? (effective + ? "Local Delegation on: frontier desk may use sealed local-agent-delegation for bounded read/evidence work" + : "Local Delegation available but currently off") + : reason; + const note = !available + ? `${esc(truncate(reason, 48))}` + : (pref === "on" && !effective + ? `requested, unavailable` + : ""); + return ` +
+ Local + + ${note} +
`; +} + +function renderSummaryBar(activeSignals, localDelegation) { const escalations = activeSignals.filter(s => s.signalType === "escalation").length; const withSignals = activeSignals.filter(s => s.signalType !== "none").length; const awaiting = activeSignals.filter(s => s.signalType === "none").length; @@ -763,13 +840,14 @@ function renderSummaryBar(activeSignals) { return `
-
+ background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;gap:12px;flex-wrap:wrap;"> +
${activeSignals.length} desk${activeSignals.length !== 1 ? "s" : ""} ${withSignals} reporting · ${awaiting} awaiting ${tokenBadge} ${calibrationBadge} ${escBadge} + ${renderLocalDelegationControl(localDelegation)}
${avgBlock}
`; @@ -945,8 +1023,9 @@ function renderStashedCard(entry) {
`; } -function renderDashboard(signals, stashed, capabilityToken) { +function renderDashboard(signals, stashed, capabilityToken, localDelegation) { const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName))); + const localDelegationState = localDelegation || currentLocalDelegationLaunch("off"); const cards = activeSignals.length > 0 ? activeSignals.map(renderSignalCard).join("") @@ -975,7 +1054,9 @@ function renderDashboard(signals, stashed, capabilityToken) { `; - const summaryBar = activeSignals.length > 0 ? renderSummaryBar(activeSignals) : ""; + // Always show the Local Delegation control so operators can see availability + // even before the first desk signal arrives. + const summaryBar = renderSummaryBar(activeSignals, localDelegationState); const stashedSection = stashed.length > 0 ? `
@@ -1058,9 +1139,13 @@ function renderDashboard(signals, stashed, capabilityToken) { const data = await res.json(); if (data.ok) { const path = data.deskPath || name; + const localNote = data.localDelegation?.effective + ? ' · local on' + : (data.localDelegation?.requested ? ' · local unavailable' : ''); if (data.launched) { // A successful open shouldn't hijack the user's clipboard. - showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path); + showToast('opening ' + name + ' desk (' + selectedProfile + localNote + ')…', + data.localDelegation?.warning || path); } else { // No terminal launched from here, so copy the path as the // fallback handle, but only claim the copy when it actually @@ -1073,12 +1158,32 @@ function renderDashboard(signals, stashed, capabilityToken) { showToast(name + ' · not found', ''); } } + async function setLocalDelegation(preference) { + const res = await fetch('/api/local-delegation?preference=' + + encodeURIComponent(preference || 'off'), POST_OPTS); + const data = await res.json(); + if (data.ok) { + const label = data.localDelegation?.effective + ? 'Local Delegation on' + : (data.localDelegation?.preference === 'on' + ? 'Local Delegation requested (unavailable)' + : 'Local Delegation off'); + showToast(label, data.localDelegation?.availability?.reason || ''); + refresh(); + } else { + showToast('Local Delegation · not updated', data.error || ''); + } + } document.addEventListener('click', (e) => { const btn = e.target.closest('button[data-act]'); if (!btn) return; + const act = btn.getAttribute('data-act'); + if (act === 'local-delegation') { + setLocalDelegation(btn.getAttribute('data-preference') || 'off'); + return; + } const name = btn.getAttribute('data-desk'); if (!name) return; - const act = btn.getAttribute('data-act'); const profile = btn.getAttribute('data-profile'); if (act === 'stash') stashDesk(name); else if (act === 'restore') restoreDesk(name); @@ -1102,19 +1207,26 @@ function renderDashboard(signals, stashed, capabilityToken) { active.getAttribute('data-act'), active.getAttribute('data-desk'), active.getAttribute('data-profile') || '', + active.getAttribute('data-preference') || '', ]); } content.innerHTML = newContent.innerHTML; if (focusKey) { - const [act, desk, profile] = JSON.parse(focusKey); - const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk; - const profileSelector = profile - ? '[data-profile="' + profile + '"]' - : ':not([data-profile])'; - const target = content.querySelector( - 'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' + - profileSelector); + const [act, desk, profile, preference] = JSON.parse(focusKey); + let target = null; + if (act === 'local-delegation') { + target = content.querySelector('button[data-act="local-delegation"]'); + } else { + const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk; + const profileSelector = profile + ? '[data-profile="' + profile + '"]' + : ':not([data-profile])'; + target = content.querySelector( + 'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' + + profileSelector); + } if (target) target.focus(); + void preference; } } } catch {} @@ -1180,6 +1292,20 @@ async function startServer(instanceId, workshopDir) { res.end(JSON.stringify({ ok: true })); return; } + if (req.method === "POST" && url.pathname === "/api/local-delegation") { + const preferenceInput = url.searchParams.get("preference") || "off"; + if (!["off", "on"].includes(String(preferenceInput).toLowerCase())) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "Invalid local delegation preference" })); + return; + } + const preference = normalizeLocalDelegationPreference(preferenceInput, "off"); + await writeLocalDelegationPreference(workshopDir, preference); + const localDelegation = currentLocalDelegationLaunch(preference); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, localDelegation })); + return; + } if (req.method === "POST" && url.pathname.startsWith("/api/open/")) { const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]); const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE; @@ -1194,14 +1320,24 @@ async function startServer(instanceId, workshopDir) { return; } const profile = normalizeDeskProfile(profileInput); + const preference = await readLocalDelegationPreference(workshopDir); + const localDelegation = currentLocalDelegationLaunch(preference); for (const subdir of ["desks", "classroom"]) { const deskPath = join(workshopDir, subdir, deskName); try { const s = await stat(deskPath); if (s.isDirectory()) { - const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile); + const launched = await launchDeskConsole( + deskPath, deskName, workshopDir, profile, localDelegation); res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile })); + res.end(JSON.stringify({ + ok: true, + deskName, + deskPath, + launched, + profile, + localDelegation, + })); return; } } catch {} @@ -1213,8 +1349,10 @@ async function startServer(instanceId, workshopDir) { const signals = await scanSignals(workshopDir); const stashed = await readStash(workshopDir); + const preference = await readLocalDelegationPreference(workshopDir); + const localDelegation = currentLocalDelegationLaunch(preference); res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(renderDashboard(signals, stashed, capabilityToken)); + res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation)); } catch (err) { // Top-level boundary: never leave a request hanging or let a // rejection become an unhandled crash — e.g. malformed %-encoding @@ -1327,7 +1465,7 @@ const session = await joinSession({ }, { name: "open_desk", - description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Returns the desk path, profile, and whether a terminal was launched.", + description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Local Delegation is orthogonal and fail-closed: when available and preferred on, the frontier desk may use sealed local-agent-delegation for bounded read/evidence work. Returns the desk path, profile, localDelegation state, and whether a terminal was launched.", inputSchema: { type: "object", properties: { @@ -1337,6 +1475,11 @@ const session = await joinSession({ enum: ["repo", "connected"], description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`, }, + localDelegation: { + type: "string", + enum: ["off", "on"], + description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).", + }, }, required: ["deskName"], }, @@ -1347,13 +1490,21 @@ const session = await joinSession({ const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE; if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" }; const profile = normalizeDeskProfile(profileInput); + const preferenceInput = ctx.input.localDelegation + ?? await readLocalDelegationPreference(entry.workshopDir); + const preference = normalizeLocalDelegationPreference(preferenceInput, "off"); + const localDelegation = currentLocalDelegationLaunch(preference); for (const subdir of ["desks", "classroom"]) { const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName); try { const s = await stat(deskPath); if (s.isDirectory()) { const launched = await launchDeskConsole( - deskPath, ctx.input.deskName, entry.workshopDir, profile); + deskPath, + ctx.input.deskName, + entry.workshopDir, + profile, + localDelegation); return { ok: true, deskName: ctx.input.deskName, @@ -1361,6 +1512,7 @@ const session = await joinSession({ launched, workshopDir: entry.workshopDir, profile, + localDelegation, }; } } catch {} diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs new file mode 100644 index 000000000..48116f3d8 --- /dev/null +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -0,0 +1,301 @@ +import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const PREFERENCES = new Set(["off", "on"]); +const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; + +export const LOCAL_DELEGATION_STATE_FILE = ".local-delegation.json"; +export const LOCAL_DELEGATION_ENV = "WORKSHOP_LOCAL_DELEGATION"; +export const LOCAL_DELEGATION_SKILL_NAME = "local-agent-delegation"; + +export function isLocalDelegationPreference(value) { + return typeof value === "string" && PREFERENCES.has(value.toLowerCase()); +} + +export function normalizeLocalDelegationPreference(value, fallback = "off") { + return isLocalDelegationPreference(value) ? value.toLowerCase() : fallback; +} + +export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) { + let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` + + `Read journal.md in this folder first to pick up where the last session ` + + `left off, then continue the desk's work. Write your journal before you stop.`; + if (localDelegationEffective) { + prompt += " Local Delegation is available for bounded, independently verifiable " + + "subtasks via the installed local-agent-delegation skill. Keep the user " + + "conversation, decomposition, judgment, and final answer in the frontier desk. " + + "Local output is a proposal and receives no savings credit unless an independent " + + "gate accepts it. Eligible V1 work is bounded read/evidence-check only; do not " + + "delegate edits, shell, broad discovery, tool authoring, or security disposition. " + + "One local failure escalates once to frontier — no retry loops."; + } + return prompt; +} + +export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) { + const env = { ...baseEnv }; + if (localDelegationEffective) { + env[LOCAL_DELEGATION_ENV] = "enabled"; + } else { + // Never leave a stale enabled flag from the parent process. + delete env[LOCAL_DELEGATION_ENV]; + } + return env; +} + +/** + * Savings credit is utilization accounting, not a price claim. + * Failed, unaccepted, redone, or escalated local work earns zero. + */ +export function localSavingsCredit({ + attempted = false, + gateAccepted = false, + redone = false, + escalated = false, +} = {}) { + if (!attempted || !gateAccepted || redone || escalated) { + return { + credit: 0, + utilization: attempted ? "handled_locally_unaccepted" : "not_attempted", + reason: !attempted + ? "not_attempted" + : escalated + ? "escalated" + : redone + ? "redone" + : "gate_not_accepted", + }; + } + return { + credit: 0, // dollar savings are never claimed by Cairn + utilization: "handled_locally_accepted", + reason: "accepted_utilization_only", + }; +} + +function isReadableFile(path) { + try { + if (!statSync(path).isFile()) return false; + accessSync(path, fsConstants.R_OK); + return true; + } catch { + return false; + } +} + +function skillMarkerPath(dir) { + return join(dir, "SKILL.md"); +} + +function looksLikeSkillDir(dir) { + return isReadableFile(skillMarkerPath(dir)); +} + +/** + * Discover the installed local-agent-delegation skill. + * Injectable probes keep unit tests filesystem-free. + */ +export function findLocalDelegationSkillDir({ + env = process.env, + home = homedir(), + exists = existsSync, + isSkillDir = looksLikeSkillDir, +} = {}) { + const explicit = (env.WORKSHOP_LOCAL_DELEGATION_SKILL_DIR || "").trim(); + if (explicit) { + return isSkillDir(explicit) ? explicit : null; + } + + const candidates = [ + join(home, ".copilot", "skills", LOCAL_DELEGATION_SKILL_NAME), + join(home, ".agents", "skills", LOCAL_DELEGATION_SKILL_NAME), + ]; + + for (const candidate of candidates) { + if (isSkillDir(candidate)) return candidate; + } + + // Inherited/plugin skills land under installed-plugins/**/skills/ + const pluginsRoot = join(home, ".copilot", "installed-plugins"); + if (exists(pluginsRoot)) { + try { + for (const plugin of readdirSync(pluginsRoot, { withFileTypes: true })) { + if (!plugin.isDirectory()) continue; + const nested = join(pluginsRoot, plugin.name, "skills", LOCAL_DELEGATION_SKILL_NAME); + if (isSkillDir(nested)) return nested; + // Some plugins nest under com.github.copilot/skills + const nested2 = join( + pluginsRoot, plugin.name, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME); + if (isSkillDir(nested2)) return nested2; + } + } catch { + // fail closed on scan errors + } + } + + return null; +} + +function readJsonFile(path, readFile = readFileSync) { + try { + return JSON.parse(readFile(path, "utf8")); + } catch { + return null; + } +} + +function isRouteId(value) { + return typeof value === "string" && SAFE_ROUTE_ID.test(value); +} + +/** + * Fail-closed availability. Enable only when skill + qualified route receipt + * (or explicit env route id) are present. Never invent availability. + */ +export function resolveLocalDelegationAvailability({ + env = process.env, + home = homedir(), + now = Date.now(), + findSkill = findLocalDelegationSkillDir, + readFile = readFileSync, + exists = existsSync, +} = {}) { + const forced = (env.WORKSHOP_LOCAL_DELEGATION_AVAILABLE || "").trim().toLowerCase(); + if (forced === "0" || forced === "false" || forced === "unavailable") { + return { + available: false, + reason: "Forced unavailable by WORKSHOP_LOCAL_DELEGATION_AVAILABLE", + skillDir: null, + routeId: null, + }; + } + + const skillDir = findSkill({ env, home, exists }); + if (!skillDir) { + return { + available: false, + reason: "local-agent-delegation skill is not installed", + skillDir: null, + routeId: null, + }; + } + + const envRoute = (env.WORKSHOP_LOCAL_DELEGATION_ROUTE_ID || "").trim(); + if (envRoute) { + if (!isRouteId(envRoute)) { + return { + available: false, + reason: "WORKSHOP_LOCAL_DELEGATION_ROUTE_ID is not a safe route id", + skillDir, + routeId: null, + }; + } + return { + available: true, + reason: "Skill installed; route id provided by environment", + skillDir, + routeId: envRoute, + }; + } + + const receiptPath = (env.WORKSHOP_LOCAL_DELEGATION_RECEIPT || "").trim() + || join(home, ".copilot", "local-agent-runs", "qualified-route.json"); + if (!exists(receiptPath)) { + return { + available: false, + reason: "No qualified route receipt (set WORKSHOP_LOCAL_DELEGATION_ROUTE_ID or write ~/.copilot/local-agent-runs/qualified-route.json)", + skillDir, + routeId: null, + }; + } + + const receipt = readJsonFile(receiptPath, readFile); + if (!receipt || typeof receipt !== "object") { + return { + available: false, + reason: "Qualified route receipt is unreadable", + skillDir, + routeId: null, + }; + } + + const status = String(receipt.status || "").toLowerCase(); + if (status !== "qualified") { + return { + available: false, + reason: `Route receipt status is '${receipt.status || "missing"}', not qualified`, + skillDir, + routeId: isRouteId(receipt.route_id || receipt.routeId) ? (receipt.route_id || receipt.routeId) : null, + }; + } + + const routeId = receipt.route_id || receipt.routeId || null; + if (!isRouteId(routeId)) { + return { + available: false, + reason: "Route receipt is missing a safe route_id", + skillDir, + routeId: null, + }; + } + + if (receipt.expires_at || receipt.expiresAt) { + const expires = Date.parse(receipt.expires_at || receipt.expiresAt); + if (!Number.isFinite(expires) || expires <= now) { + return { + available: false, + reason: "Qualified route receipt has expired", + skillDir, + routeId, + }; + } + } + + return { + available: true, + reason: "Skill installed; qualified route receipt present", + skillDir, + routeId, + }; +} + +export function resolveLocalDelegationLaunch({ + preference = "off", + availability, +} = {}) { + const pref = normalizeLocalDelegationPreference(preference, "off"); + const available = Boolean(availability?.available); + if (pref === "on" && !available) { + return { + preference: pref, + requested: true, + effective: false, + availability, + warning: availability?.reason || "Local Delegation unavailable", + }; + } + return { + preference: pref, + requested: pref === "on", + effective: pref === "on" && available, + availability, + warning: null, + }; +} + +export function parseLocalDelegationState(raw) { + if (!raw || typeof raw !== "object") { + return { preference: "off" }; + } + return { + preference: normalizeLocalDelegationPreference(raw.preference, "off"), + }; +} + +export function serializeLocalDelegationState(state) { + return { + preference: normalizeLocalDelegationPreference(state?.preference, "off"), + updatedAt: new Date().toISOString(), + }; +} diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs new file mode 100644 index 000000000..cd4c5c55e --- /dev/null +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -0,0 +1,190 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + buildLocalDelegationLaunchEnv, + deskOrientPrompt, + findLocalDelegationSkillDir, + isLocalDelegationPreference, + localSavingsCredit, + normalizeLocalDelegationPreference, + parseLocalDelegationState, + resolveLocalDelegationAvailability, + resolveLocalDelegationLaunch, + serializeLocalDelegationState, +} from "./local-delegation.mjs"; +import { + buildDeskAgentArgv, + normalizeDeskProfile, +} from "./launch-profile.mjs"; + +test("normalizes local-delegation preference independently of desk profile", () => { + assert.equal(isLocalDelegationPreference("on"), true); + assert.equal(isLocalDelegationPreference("OFF"), true); + assert.equal(isLocalDelegationPreference("maybe"), false); + assert.equal(normalizeLocalDelegationPreference("ON"), "on"); + assert.equal(normalizeLocalDelegationPreference("nope"), "off"); + // Profile axis remains orthogonal and untouched. + assert.equal(normalizeDeskProfile("connected"), "connected"); + assert.equal(normalizeDeskProfile("repo"), "repo"); +}); + +test("availability is fail-closed without skill or route receipt", () => { + const missingSkill = resolveLocalDelegationAvailability({ + env: {}, + home: "C:\\home", + findSkill: () => null, + exists: () => false, + }); + assert.equal(missingSkill.available, false); + assert.match(missingSkill.reason, /not installed/i); + + const skillOnly = resolveLocalDelegationAvailability({ + env: {}, + home: "C:\\home", + findSkill: () => "C:\\home\\.copilot\\skills\\local-agent-delegation", + exists: () => false, + }); + assert.equal(skillOnly.available, false); + assert.match(skillOnly.reason, /No qualified route receipt/i); +}); + +test("availability accepts env route id or a qualified receipt", () => { + const viaEnv = resolveLocalDelegationAvailability({ + env: { WORKSHOP_LOCAL_DELEGATION_ROUTE_ID: "foundry-qwen25-7b-qualified" }, + home: "C:\\home", + findSkill: () => "C:\\skills\\local-agent-delegation", + exists: () => false, + }); + assert.equal(viaEnv.available, true); + assert.equal(viaEnv.routeId, "foundry-qwen25-7b-qualified"); + + const receiptPath = "C:\\home\\.copilot\\local-agent-runs\\qualified-route.json"; + const viaReceipt = resolveLocalDelegationAvailability({ + env: {}, + home: "C:\\home", + now: Date.parse("2026-08-14T12:00:00Z"), + findSkill: () => "C:\\skills\\local-agent-delegation", + exists: (p) => p === receiptPath, + readFile: () => JSON.stringify({ + status: "qualified", + route_id: "foundry-qwen25-7b-qualified", + expires_at: "2026-12-01T00:00:00Z", + }), + }); + assert.equal(viaReceipt.available, true); + assert.equal(viaReceipt.routeId, "foundry-qwen25-7b-qualified"); + + const expired = resolveLocalDelegationAvailability({ + env: {}, + home: "C:\\home", + now: Date.parse("2027-01-01T00:00:00Z"), + findSkill: () => "C:\\skills\\local-agent-delegation", + exists: (p) => p === receiptPath, + readFile: () => JSON.stringify({ + status: "qualified", + route_id: "foundry-qwen25-7b-qualified", + expires_at: "2026-12-01T00:00:00Z", + }), + }); + assert.equal(expired.available, false); + assert.match(expired.reason, /expired/i); +}); + +test("requested on + unavailable stays ineffective with a warning", () => { + const launch = resolveLocalDelegationLaunch({ + preference: "on", + availability: { + available: false, + reason: "local-agent-delegation skill is not installed", + }, + }); + assert.equal(launch.requested, true); + assert.equal(launch.effective, false); + assert.match(launch.warning, /not installed/i); +}); + +test("launch env and prompt only enable when effective", () => { + const base = { PATH: "/usr/bin", WORKSHOP_LOCAL_DELEGATION: "enabled" }; + const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false }); + assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false); + assert.equal(offEnv.PATH, "/usr/bin"); + + const onEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: true }); + assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled"); + + const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false }); + assert.equal(offPrompt.includes("Local Delegation is available"), false); + assert.match(offPrompt, /cost-desk/); + + const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true }); + assert.match(onPrompt, /Local Delegation is available/); + assert.match(onPrompt, /local-agent-delegation/); + assert.match(onPrompt, /no savings credit/i); +}); + +test("repo/connected argv stays orthogonal to local-delegation preference", () => { + const repo = buildDeskAgentArgv({ + deskName: "cost-desk", + workshopDir: "/workshop", + useAgency: false, + copilotCommand: "copilot", + profile: "repo", + pluginMcpNames: ["teams"], + }); + const connected = buildDeskAgentArgv({ + deskName: "cost-desk", + workshopDir: "/workshop", + useAgency: false, + copilotCommand: "copilot", + profile: "connected", + pluginMcpNames: ["teams"], + }); + assert.deepEqual(repo, [ + "copilot", "--name", "cost-desk", + "--disable-mcp-server", "teams", + "--add-dir", "/workshop", + ]); + assert.deepEqual(connected, [ + "copilot", "--name", "cost-desk", + "--add-dir", "/workshop", + ]); + // Local delegation never injects into argv — only env/prompt. + assert.equal(repo.includes("local"), false); + assert.equal(connected.includes("local"), false); +}); + +test("failed or unaccepted local work earns zero savings credit", () => { + assert.equal(localSavingsCredit({ attempted: false }).credit, 0); + assert.equal(localSavingsCredit({ attempted: true, gateAccepted: false }).credit, 0); + assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, redone: true }).credit, 0); + assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, escalated: true }).credit, 0); + const accepted = localSavingsCredit({ attempted: true, gateAccepted: true }); + assert.equal(accepted.credit, 0); + assert.equal(accepted.utilization, "handled_locally_accepted"); +}); + +test("state parse/serialize defaults to off", () => { + assert.deepEqual(parseLocalDelegationState(null), { preference: "off" }); + assert.equal(parseLocalDelegationState({ preference: "ON" }).preference, "on"); + const serialized = serializeLocalDelegationState({ preference: "on" }); + assert.equal(serialized.preference, "on"); + assert.equal(typeof serialized.updatedAt, "string"); +}); + +test("skill discovery respects explicit dir and common install roots", () => { + const found = findLocalDelegationSkillDir({ + env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\sealed\\.github\\skills\\local-agent-delegation" }, + home: "C:\\home", + exists: () => false, + isSkillDir: (p) => p === "D:\\sealed\\.github\\skills\\local-agent-delegation", + }); + assert.equal(found, "D:\\sealed\\.github\\skills\\local-agent-delegation"); + + const missingExplicit = findLocalDelegationSkillDir({ + env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\missing" }, + home: "C:\\home", + exists: () => false, + isSkillDir: () => false, + }); + assert.equal(missingExplicit, null); +}); diff --git a/extensions/signals-dashboard/package.json b/extensions/signals-dashboard/package.json index 6165e97c6..5fa278592 100644 --- a/extensions/signals-dashboard/package.json +++ b/extensions/signals-dashboard/package.json @@ -1,15 +1,15 @@ { "name": "signals-dashboard", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "main": "extension.mjs", "scripts": { - "test": "node --test launch-profile.test.mjs" + "test": "node --test launch-profile.test.mjs local-delegation.test.mjs" }, "dependencies": { "@github/copilot-sdk": "latest" }, - "description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", + "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.", "keywords": [ "agent-signals", "dashboard", diff --git a/plugins/signals-dashboard/plugin.json b/plugins/signals-dashboard/plugin.json index 65db37c08..00059d8fe 100644 --- a/plugins/signals-dashboard/plugin.json +++ b/plugins/signals-dashboard/plugin.json @@ -1,7 +1,7 @@ { "name": "signals-dashboard", - "description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", - "version": "0.2.0", + "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.", + "version": "0.3.0", "author": { "name": "jennyf19", "url": "https://github.com/jennyf19" @@ -11,7 +11,8 @@ "dashboard", "multi-agent", "coordination", - "canvas" + "canvas", + "local-delegation" ], "extensions": { "com.github.copilot": { diff --git a/plugins/the-workshop/README.md b/plugins/the-workshop/README.md index a0aecb3f8..fc0c7cca8 100644 --- a/plugins/the-workshop/README.md +++ b/plugins/the-workshop/README.md @@ -45,6 +45,13 @@ 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. +The summary bar also has a **Local** toggle. It is orthogonal to repo/connected: +when available and on, the frontier desk may use +[Sealed Delegation](https://github.com/jennyf19/sealed-delegation)'s +`local-agent-delegation` skill for bounded read/evidence work. Availability is +fail-closed (installed skill + qualified route receipt). Local output is a +proposal and never earns savings credit on its own. + 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. From 76953086c5db6d1648b78d86aff57724c0fd3d8d Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 10:48:29 -0700 Subject: [PATCH 2/9] fix: address Local Delegation review (skill path + README catalog) - Walk marketplace/plugin and _direct install roots for local-agent-delegation - Update plugins/signals-dashboard README and regenerate docs/README.plugins.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- docs/README.plugins.md | 2 +- .../signals-dashboard/local-delegation.mjs | 34 ++++++++++++++----- .../local-delegation.test.mjs | 28 +++++++++++++++ plugins/signals-dashboard/README.md | 18 ++++++++-- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/docs/README.plugins.md b/docs/README.plugins.md index 3e9089d74..35569da3b 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -102,7 +102,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [rust-mcp-development](../plugins/rust-mcp-development/README.md) | Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations. | 2 items | rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp | | [salesforce-development](../plugins/salesforce-development/README.md) | Complete Salesforce agentic development environment covering Apex & Triggers, Flow automation, Lightning Web Components, Aura components, and Visualforce pages. | 7 items | salesforce, apex, triggers, lwc, aura, flow, visualforce, crm, salesforce-dx | | [security-best-practices](../plugins/security-best-practices/README.md) | Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications. | 1 items | security, accessibility, performance, code-quality, owasp, a11y, optimization, best-practices | -| [signals-dashboard](../plugins/signals-dashboard/README.md) | Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles. | 1 items | agent-signals, dashboard, multi-agent, coordination, canvas | +| [signals-dashboard](../plugins/signals-dashboard/README.md) | Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation. | 1 items | agent-signals, dashboard, multi-agent, coordination, canvas, local-delegation | | [site-studio](../plugins/site-studio/README.md) | Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change. | 1 items | agent-collaboration, content-authoring, personal-website, progress-tracking, section-planning, site-builder | | [skill-image-gen](../plugins/skill-image-gen/README.md) | Generate images using AI directly from your coding workflow. Supports OpenAI (gpt-image-2) and Google Gemini. BYO API key — the skill guides you through setup on first use. | 1 items | image-generation, openai, gemini, ai, art, sprites, textures, icons | | [software-engineering-team](../plugins/software-engineering-team/README.md) | 7 specialized agents covering the full software development lifecycle from UX design and architecture to security and DevOps. | 7 items | team, enterprise, security, devops, ux, architecture, product, ai-ethics | diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index 48116f3d8..f4ec1f557 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -116,18 +116,34 @@ export function findLocalDelegationSkillDir({ if (isSkillDir(candidate)) return candidate; } - // Inherited/plugin skills land under installed-plugins/**/skills/ + // Marketplace: ~/.copilot/installed-plugins/// + // Direct: ~/.copilot/installed-plugins/_direct// + // Skill dirs may live at skills/, .github/skills/, or com.github.copilot/skills/. const pluginsRoot = join(home, ".copilot", "installed-plugins"); if (exists(pluginsRoot)) { try { - for (const plugin of readdirSync(pluginsRoot, { withFileTypes: true })) { - if (!plugin.isDirectory()) continue; - const nested = join(pluginsRoot, plugin.name, "skills", LOCAL_DELEGATION_SKILL_NAME); - if (isSkillDir(nested)) return nested; - // Some plugins nest under com.github.copilot/skills - const nested2 = join( - pluginsRoot, plugin.name, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME); - if (isSkillDir(nested2)) return nested2; + for (const market of readdirSync(pluginsRoot, { withFileTypes: true })) { + if (!market.isDirectory()) continue; + const marketRoot = join(pluginsRoot, market.name); + let pluginEntries; + try { + pluginEntries = readdirSync(marketRoot, { withFileTypes: true }); + } catch { + continue; + } + for (const plugin of pluginEntries) { + if (!plugin.isDirectory()) continue; + const pluginRoot = join(marketRoot, plugin.name); + const nestedCandidates = [ + join(pluginRoot, "skills", LOCAL_DELEGATION_SKILL_NAME), + join(pluginRoot, ".github", "skills", LOCAL_DELEGATION_SKILL_NAME), + join(pluginRoot, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME), + join(pluginRoot, "com.github.awesome-copilot", "skills", LOCAL_DELEGATION_SKILL_NAME), + ]; + for (const nested of nestedCandidates) { + if (isSkillDir(nested)) return nested; + } + } } } catch { // fail closed on scan errors diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index cd4c5c55e..7f444efa0 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -188,3 +188,31 @@ test("skill discovery respects explicit dir and common install roots", () => { }); assert.equal(missingExplicit, null); }); + +test("skill discovery walks marketplace/plugin and _direct install layouts", async () => { + const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const home = mkdtempSync(join(tmpdir(), "ld-skill-")); + try { + const skillDir = join( + home, ".copilot", "installed-plugins", "awesome-copilot", "sealed-delegation", + ".github", "skills", "local-agent-delegation"); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, "SKILL.md"), "# local-agent-delegation\n"); + const found = findLocalDelegationSkillDir({ home, env: {} }); + assert.equal(found, skillDir); + + const direct = join( + home, ".copilot", "installed-plugins", "_direct", "sealed-delegation", + "skills", "local-agent-delegation"); + // Prefer first match; after removing marketplace tree, direct should hit. + rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true }); + mkdirSync(direct, { recursive: true }); + writeFileSync(join(direct, "SKILL.md"), "# local-agent-delegation\n"); + const foundDirect = findLocalDelegationSkillDir({ home, env: {} }); + assert.equal(foundDirect, direct); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); diff --git a/plugins/signals-dashboard/README.md b/plugins/signals-dashboard/README.md index 96aed60c4..a97e81bcd 100644 --- a/plugins/signals-dashboard/README.md +++ b/plugins/signals-dashboard/README.md @@ -1,15 +1,27 @@ # Signals Dashboard Plugin -Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles. +Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware +desk profiles (`repo` / `connected`), and fail-closed Local Delegation. ## Installation -``bash +```bash copilot plugin install signals-dashboard@awesome-copilot -`` +``` + +## Features + +- Live desk signals, score bars, patterns, and escalations +- Cost-aware **open** (repo) and **connected** desk launch profiles +- **Local Delegation** toggle: when available, the frontier desk may use the + installed `local-agent-delegation` skill for bounded read/evidence work +- Fail-closed availability (skill + qualified route receipt); no silent savings credit ## Source +Canonical implementation: [jennyf19/the-workshop](https://github.com/jennyf19/the-workshop). +Local worker runtime: [jennyf19/sealed-delegation](https://github.com/jennyf19/sealed-delegation). + This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot). ## License From 6bcde19d77cc6a693559c48bed4af24b037103ba Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 11:02:07 -0700 Subject: [PATCH 3/9] fix: keep Local Delegation Windows launches from reparsing -i prompt Long LD orientation text split under wt/cmd on Windows (0x80070002). Keep -i short/quote-free; rely on WORKSHOP_LOCAL_DELEGATION=enabled (+ skill) for policy. Add charset/length guard tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/README.md | 4 ++- .../signals-dashboard/local-delegation.mjs | 32 +++++++++++++------ .../local-delegation.test.mjs | 20 ++++++------ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/extensions/signals-dashboard/README.md b/extensions/signals-dashboard/README.md index 0a54169eb..e88527ba6 100644 --- a/extensions/signals-dashboard/README.md +++ b/extensions/signals-dashboard/README.md @@ -112,7 +112,9 @@ When the preference is on but availability fails, opens still launch as frontier desks and surface the reason — they never silently fall back with savings credit. When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched -process and appends a short orientation note that local output is a proposal. +process. The desk orientation `-i` prompt stays the short base journal prompt +(Windows Terminal cannot safely reparse a long multi-sentence `-i` string). +Policy lives in that env flag plus the installed `local-agent-delegation` skill. The runtime, launcher, and gates remain owned by [Sealed Delegation](https://github.com/jennyf19/sealed-delegation). diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index f4ec1f557..828a6479d 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -17,19 +17,31 @@ export function normalizeLocalDelegationPreference(value, fallback = "off") { return isLocalDelegationPreference(value) ? value.toLowerCase() : fallback; } +// Windows Terminal / cmd reparse cannot safely carry long multi-space -i strings. +// Keep the orientation prompt short, ASCII, and quote-free. Local Delegation +// policy lives in WORKSHOP_LOCAL_DELEGATION=enabled + the installed skill — not +// on the CLI. +const MAX_ORIENT_PROMPT_CHARS = 280; +// Allow apostrophes (desk's). Ban double quotes, backticks, dashes that WT/cmd +// have split on, and classic cmd metacharacters. +const UNSAFE_ORIENT_CHARS = /["`—–|&<>^%!()\r\n]/; + +export function isSafeDeskOrientPrompt(prompt) { + return typeof prompt === "string" + && prompt.length > 0 + && prompt.length <= MAX_ORIENT_PROMPT_CHARS + && !UNSAFE_ORIENT_CHARS.test(prompt); +} + export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) { - let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` + + // deskName is already constrained to a slug by the launcher; still keep the + // prompt free of punctuation that cmd/wt have historically mis-parsed. + const prompt = `You are sitting down at the ${deskName} desk in this workshop. ` + `Read journal.md in this folder first to pick up where the last session ` + `left off, then continue the desk's work. Write your journal before you stop.`; - if (localDelegationEffective) { - prompt += " Local Delegation is available for bounded, independently verifiable " + - "subtasks via the installed local-agent-delegation skill. Keep the user " + - "conversation, decomposition, judgment, and final answer in the frontier desk. " + - "Local output is a proposal and receives no savings credit unless an independent " + - "gate accepts it. Eligible V1 work is bounded read/evidence-check only; do not " + - "delegate edits, shell, broad discovery, tool authoring, or security disposition. " + - "One local failure escalates once to frontier — no retry loops."; - } + // localDelegationEffective is intentionally unused in the -i string: policy + // is carried by env + skill so Windows launch cannot reparse a long appendix. + void localDelegationEffective; return prompt; } diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index 7f444efa0..962a8013c 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -5,6 +5,7 @@ import { deskOrientPrompt, findLocalDelegationSkillDir, isLocalDelegationPreference, + isSafeDeskOrientPrompt, localSavingsCredit, normalizeLocalDelegationPreference, parseLocalDelegationState, @@ -103,7 +104,7 @@ test("requested on + unavailable stays ineffective with a warning", () => { assert.match(launch.warning, /not installed/i); }); -test("launch env and prompt only enable when effective", () => { +test("launch env enables when effective; -i prompt stays short and quote-free", () => { const base = { PATH: "/usr/bin", WORKSHOP_LOCAL_DELEGATION: "enabled" }; const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false }); assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false); @@ -113,13 +114,15 @@ test("launch env and prompt only enable when effective", () => { assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled"); const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false }); - assert.equal(offPrompt.includes("Local Delegation is available"), false); - assert.match(offPrompt, /cost-desk/); - const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true }); - assert.match(onPrompt, /Local Delegation is available/); - assert.match(onPrompt, /local-agent-delegation/); - assert.match(onPrompt, /no savings credit/i); + // Policy must not ride on -i (Windows wt/cmd reparse splits long LD text). + assert.equal(offPrompt, onPrompt); + assert.match(onPrompt, /cost-desk/); + assert.equal(onPrompt.includes("Local Delegation"), false); + assert.equal(onPrompt.includes("do not delegate"), false); + assert.equal(isSafeDeskOrientPrompt(onPrompt), true); + assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false); + assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false); }); test("repo/connected argv stays orthogonal to local-delegation preference", () => { @@ -203,11 +206,10 @@ test("skill discovery walks marketplace/plugin and _direct install layouts", asy const found = findLocalDelegationSkillDir({ home, env: {} }); assert.equal(found, skillDir); + rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true }); const direct = join( home, ".copilot", "installed-plugins", "_direct", "sealed-delegation", "skills", "local-agent-delegation"); - // Prefer first match; after removing marketplace tree, direct should hit. - rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true }); mkdirSync(direct, { recursive: true }); writeFileSync(join(direct, "SKILL.md"), "# local-agent-delegation\n"); const foundDirect = findLocalDelegationSkillDir({ home, env: {} }); From 322ff131afa36d9f17cda505586cde571b86dee6 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:15:52 -0700 Subject: [PATCH 4/9] fix: surface Local Delegation effective state on open toast and badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep -i short with one optional ASCII line. Show operators "Local Delegation effective · route " on open and in the summary control. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/README.md | 8 ++-- extensions/signals-dashboard/extension.mjs | 34 ++++++++++------ .../signals-dashboard/local-delegation.mjs | 40 +++++++++++++++++-- .../local-delegation.test.mjs | 32 +++++++++++++-- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/extensions/signals-dashboard/README.md b/extensions/signals-dashboard/README.md index e88527ba6..7f5a56d88 100644 --- a/extensions/signals-dashboard/README.md +++ b/extensions/signals-dashboard/README.md @@ -112,9 +112,11 @@ When the preference is on but availability fails, opens still launch as frontier desks and surface the reason — they never silently fall back with savings credit. When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched -process. The desk orientation `-i` prompt stays the short base journal prompt -(Windows Terminal cannot safely reparse a long multi-sentence `-i` string). -Policy lives in that env flag plus the installed `local-agent-delegation` skill. +process, shows an open toast/badge (`Local Delegation effective · route …`), and +may append one short ASCII line to `-i` (`Local Delegation env is enabled.`) when +that combined prompt stays quote-free and under the length guard. Full policy +still lives in the env flag plus the installed `local-agent-delegation` skill — +never a long multi-sentence `-i` appendix (Windows Terminal reparse). The runtime, launcher, and gates remain owned by [Sealed Delegation](https://github.com/jennyf19/sealed-delegation). diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index 53b0f56cc..0f6310ad0 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -23,6 +23,7 @@ import { LOCAL_DELEGATION_STATE_FILE, buildLocalDelegationLaunchEnv, deskOrientPrompt, + formatLocalDelegationOpenNotice, normalizeLocalDelegationPreference, parseLocalDelegationState, resolveLocalDelegationAvailability, @@ -779,16 +780,19 @@ function renderLocalDelegationControl(localDelegation) { const available = Boolean(localDelegation?.availability?.available); const effective = Boolean(localDelegation?.effective); const reason = localDelegation?.availability?.reason || "Local Delegation unavailable"; + const routeId = localDelegation?.availability?.routeId || null; const next = pref === "on" ? "off" : "on"; const label = effective ? "On" : (pref === "on" ? "On*" : "Off"); const color = effective ? "#86efac" : (pref === "on" ? "#fbbf24" : "#94a3b8"); const border = effective ? "#166534" : (pref === "on" ? "#854d0e" : "#334155"); const title = available ? (effective - ? "Local Delegation on: frontier desk may use sealed local-agent-delegation for bounded read/evidence work" + ? `Local Delegation effective${routeId ? ` · route ${routeId}` : ""}` : "Local Delegation available but currently off") : reason; - const note = !available + const note = effective && routeId + ? `effective · ${esc(truncate(routeId, 28))}` + : !available ? `${esc(truncate(reason, 48))}` : (pref === "on" && !effective ? `requested, unavailable` @@ -1139,20 +1143,22 @@ function renderDashboard(signals, stashed, capabilityToken, localDelegation) { const data = await res.json(); if (data.ok) { const path = data.deskPath || name; - const localNote = data.localDelegation?.effective - ? ' · local on' - : (data.localDelegation?.requested ? ' · local unavailable' : ''); + const notice = data.localDelegationNotice || {}; + const localTitle = notice.titleSuffix || ''; + const localDetail = notice.detail || ''; if (data.launched) { // A successful open shouldn't hijack the user's clipboard. - showToast('opening ' + name + ' desk (' + selectedProfile + localNote + ')…', - data.localDelegation?.warning || path); + // Surface LD state in the toast — operators cannot rely on -i alone. + showToast('opening ' + name + ' desk (' + selectedProfile + localTitle + ')…', + localDetail || path); } else { // No terminal launched from here, so copy the path as the // fallback handle, but only claim the copy when it actually // succeeded. The path shows in the toast either way. let copied = false; try { await navigator.clipboard.writeText(path); copied = true; } catch {} - showToast(copied ? (name + ' · path copied') : (name + ' · copy this path'), path); + const copyTitle = copied ? (name + ' · path copied') : (name + ' · copy this path'); + showToast(copyTitle + localTitle, localDetail || path); } } else { showToast(name + ' · not found', ''); @@ -1163,12 +1169,14 @@ function renderDashboard(signals, stashed, capabilityToken, localDelegation) { encodeURIComponent(preference || 'off'), POST_OPTS); const data = await res.json(); if (data.ok) { - const label = data.localDelegation?.effective - ? 'Local Delegation on' - : (data.localDelegation?.preference === 'on' + const ld = data.localDelegation || {}; + const routeId = ld.availability && ld.availability.routeId; + const label = ld.effective + ? ('Local Delegation effective' + (routeId ? (' · route ' + routeId) : '')) + : (ld.preference === 'on' ? 'Local Delegation requested (unavailable)' : 'Local Delegation off'); - showToast(label, data.localDelegation?.availability?.reason || ''); + showToast(label, ld.availability?.reason || ''); refresh(); } else { showToast('Local Delegation · not updated', data.error || ''); @@ -1337,6 +1345,7 @@ async function startServer(instanceId, workshopDir) { launched, profile, localDelegation, + localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation), })); return; } @@ -1513,6 +1522,7 @@ const session = await joinSession({ workshopDir: entry.workshopDir, profile, localDelegation, + localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation), }; } } catch {} diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index 828a6479d..270374a8d 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -33,18 +33,50 @@ export function isSafeDeskOrientPrompt(prompt) { && !UNSAFE_ORIENT_CHARS.test(prompt); } +/** One short ASCII notice operators can see in the session start prompt. */ +export const LOCAL_DELEGATION_ORIENT_LINE = " Local Delegation env is enabled."; + export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) { // deskName is already constrained to a slug by the launcher; still keep the // prompt free of punctuation that cmd/wt have historically mis-parsed. - const prompt = `You are sitting down at the ${deskName} desk in this workshop. ` + + let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` + `Read journal.md in this folder first to pick up where the last session ` + `left off, then continue the desk's work. Write your journal before you stop.`; - // localDelegationEffective is intentionally unused in the -i string: policy - // is carried by env + skill so Windows launch cannot reparse a long appendix. - void localDelegationEffective; + // Only a single short ASCII line may ride on -i. Full policy stays in env + skill. + if (localDelegationEffective) { + const withNotice = prompt + LOCAL_DELEGATION_ORIENT_LINE; + if (isSafeDeskOrientPrompt(withNotice)) prompt = withNotice; + } return prompt; } +/** + * Operator-visible summary for open toasts and badges. + * Never claims savings; only reports effective state + route id when known. + */ +export function formatLocalDelegationOpenNotice(localDelegation) { + if (!localDelegation || typeof localDelegation !== "object") { + return { titleSuffix: "", detail: "" }; + } + const routeId = localDelegation.availability?.routeId || null; + if (localDelegation.effective) { + const routePart = routeId ? ` · route ${routeId}` : ""; + return { + titleSuffix: " · Local Delegation effective", + detail: `Local Delegation effective${routePart}`, + }; + } + if (localDelegation.requested) { + return { + titleSuffix: " · local unavailable", + detail: localDelegation.warning + || localDelegation.availability?.reason + || "Local Delegation requested but unavailable", + }; + } + return { titleSuffix: "", detail: "" }; +} + export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) { const env = { ...baseEnv }; if (localDelegationEffective) { diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index 962a8013c..0449764f1 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -1,9 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; import { + LOCAL_DELEGATION_ORIENT_LINE, buildLocalDelegationLaunchEnv, deskOrientPrompt, findLocalDelegationSkillDir, + formatLocalDelegationOpenNotice, isLocalDelegationPreference, isSafeDeskOrientPrompt, localSavingsCredit, @@ -115,16 +117,38 @@ test("launch env enables when effective; -i prompt stays short and quote-free", const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false }); const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true }); - // Policy must not ride on -i (Windows wt/cmd reparse splits long LD text). - assert.equal(offPrompt, onPrompt); - assert.match(onPrompt, /cost-desk/); - assert.equal(onPrompt.includes("Local Delegation"), false); + assert.match(offPrompt, /cost-desk/); + assert.equal(offPrompt.includes("Local Delegation"), false); + // One short ASCII line only — never the long policy appendix. + assert.equal(onPrompt, offPrompt + LOCAL_DELEGATION_ORIENT_LINE); + assert.match(onPrompt, /Local Delegation env is enabled\./); assert.equal(onPrompt.includes("do not delegate"), false); assert.equal(isSafeDeskOrientPrompt(onPrompt), true); assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false); assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false); }); +test("open notice reports effective route without savings claims", () => { + assert.deepEqual(formatLocalDelegationOpenNotice({ + effective: true, + requested: true, + availability: { available: true, routeId: "foundry-qwen25-7b-qualified" }, + }), { + titleSuffix: " · Local Delegation effective", + detail: "Local Delegation effective · route foundry-qwen25-7b-qualified", + }); + assert.deepEqual(formatLocalDelegationOpenNotice({ + effective: false, + requested: true, + warning: "local-agent-delegation skill is not installed", + availability: { available: false, reason: "local-agent-delegation skill is not installed" }, + }).detail, "local-agent-delegation skill is not installed"); + assert.deepEqual(formatLocalDelegationOpenNotice({ + effective: false, + requested: false, + }), { titleSuffix: "", detail: "" }); +}); + test("repo/connected argv stays orthogonal to local-delegation preference", () => { const repo = buildDeskAgentArgv({ deskName: "cost-desk", From f5a8b31c25bbcdb7474dbf9d320e0599150af779 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:25:26 -0700 Subject: [PATCH 5/9] fix: address Local Delegation GHCP review comments - Cross-platform path.join in receipt fixture - Atomic .local-delegation.json write (no symlink follow) - Case-insensitive WORKSHOP_LOCAL_DELEGATION env clear - Windows Terminal: set/clear env via cmd before agent - aria-pressed on Local toggle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/extension.mjs | 59 ++++++++++++------- .../signals-dashboard/local-delegation.mjs | 20 ++++++- .../local-delegation.test.mjs | 35 ++++++++--- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index 0f6310ad0..c22d752fc 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -5,7 +5,7 @@ import { createServer } from "node:http"; import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs"; -import { readdir, readFile, writeFile, stat } from "node:fs/promises"; +import { readdir, readFile, writeFile, stat, rename, unlink } from "node:fs/promises"; import { join, delimiter, isAbsolute, sep } from "node:path"; import { spawn } from "node:child_process"; import { randomBytes } from "node:crypto"; @@ -29,6 +29,7 @@ import { resolveLocalDelegationAvailability, resolveLocalDelegationLaunch, serializeLocalDelegationState, + windowsLocalDelegationCmdPrefix, } from "./local-delegation.mjs"; const servers = new Map(); @@ -376,10 +377,27 @@ async function readLocalDelegationPreference(workshopDir) { async function writeLocalDelegationPreference(workshopDir, preference) { const state = serializeLocalDelegationState({ preference }); - await writeFile( - join(workshopDir, LOCAL_DELEGATION_STATE_FILE), - JSON.stringify(state, null, 2) + "\n", - "utf8"); + const target = join(workshopDir, LOCAL_DELEGATION_STATE_FILE); + // Write via a same-directory temp file + rename so a checked-in symlink at + // .local-delegation.json cannot redirect the write outside the workshop. + const tmp = join( + workshopDir, + `.local-delegation.${process.pid}.${randomBytes(4).toString("hex")}.tmp`); + const body = JSON.stringify(state, null, 2) + "\n"; + try { + await writeFile(tmp, body, { encoding: "utf8", flag: "wx" }); + try { + await rename(tmp, target); + } catch { + // Windows may refuse rename-over-existing; replace without following + // a dangling external target by unlinking the name first. + await unlink(target).catch(() => {}); + await rename(tmp, target); + } + } catch (err) { + await unlink(tmp).catch(() => {}); + throw err; + } return state; } @@ -413,20 +431,20 @@ async function launchDeskConsole( if (process.platform === "win32") { const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir }); const cmd = resolveSystem32Executable("cmd.exe"); - const direct = /\.(exe|com)$/i.test(run[0]); - if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run], { env })) return true; - - // Older installs can expose .cmd/.bat shims. Only use cmd.exe when every - // argument is free of cmd metacharacters; otherwise fail closed and let - // the UI copy the desk path rather than reparse an unsafe workshop path. - const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)); - if (cmdSafe && wt && cmd && - await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run], { env })) return true; - // Fallback when wt.exe is absent: a fresh console window via `start`, - // still through cmd /k only when the arguments are safe for reparsing. - return cmdSafe && cmd - ? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath, env }) - : false; + // wt.exe does not reliably forward Node's spawn env into a new tab when + // Windows Terminal is already running. Always start through cmd.exe and + // set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself. + const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)) + && run.every((arg) => isSafeWindowsCmdShim(arg)); + if (!cmdSafe || !cmd) return false; + const inner = windowsLocalDelegationCmdPrefix(effective) + + run.map(quoteWindowsCmdArgument).join(" "); + if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) { + return true; + } + // Fallback when wt.exe is absent: a fresh console window via `start`. + return await trySpawn( + cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env }); } if (process.platform === "darwin") { const osascript = "/usr/bin/osascript"; @@ -801,7 +819,8 @@ function renderLocalDelegationControl(localDelegation) {
Local diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index 270374a8d..759ff0fed 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -79,15 +79,29 @@ export function formatLocalDelegationOpenNotice(localDelegation) { export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) { const env = { ...baseEnv }; + // Windows env names are case-insensitive; spreading process.env yields a + // case-sensitive object, so clear every spelling before optionally setting. + const target = LOCAL_DELEGATION_ENV.toLowerCase(); + for (const key of Object.keys(env)) { + if (key.toLowerCase() === target) delete env[key]; + } if (localDelegationEffective) { env[LOCAL_DELEGATION_ENV] = "enabled"; - } else { - // Never leave a stale enabled flag from the parent process. - delete env[LOCAL_DELEGATION_ENV]; } return env; } +/** + * cmd.exe prefix that forces WORKSHOP_LOCAL_DELEGATION on or off inside a new + * Windows Terminal / console session. wt.exe does not reliably forward the + * caller's process env into a new tab when Terminal is already running. + */ +export function windowsLocalDelegationCmdPrefix(localDelegationEffective = false) { + return localDelegationEffective + ? 'set "WORKSHOP_LOCAL_DELEGATION=enabled"&& ' + : 'set "WORKSHOP_LOCAL_DELEGATION="&& '; +} + /** * Savings credit is utilization accounting, not a price claim. * Failed, unaccepted, redone, or escalated local work earns zero. diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index 0449764f1..d066c9e67 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -1,5 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { join } from "node:path"; import { LOCAL_DELEGATION_ORIENT_LINE, buildLocalDelegationLaunchEnv, @@ -14,10 +15,12 @@ import { resolveLocalDelegationAvailability, resolveLocalDelegationLaunch, serializeLocalDelegationState, + windowsLocalDelegationCmdPrefix, } from "./local-delegation.mjs"; import { buildDeskAgentArgv, normalizeDeskProfile, + quoteWindowsCmdArgument, } from "./launch-profile.mjs"; test("normalizes local-delegation preference independently of desk profile", () => { @@ -52,21 +55,24 @@ test("availability is fail-closed without skill or route receipt", () => { }); test("availability accepts env route id or a qualified receipt", () => { + const home = join("home"); + const skillDir = join("skills", "local-agent-delegation"); const viaEnv = resolveLocalDelegationAvailability({ env: { WORKSHOP_LOCAL_DELEGATION_ROUTE_ID: "foundry-qwen25-7b-qualified" }, - home: "C:\\home", - findSkill: () => "C:\\skills\\local-agent-delegation", + home, + findSkill: () => skillDir, exists: () => false, }); assert.equal(viaEnv.available, true); assert.equal(viaEnv.routeId, "foundry-qwen25-7b-qualified"); - const receiptPath = "C:\\home\\.copilot\\local-agent-runs\\qualified-route.json"; + // Match host path.join separators so Linux CI exercises the receipt branch. + const receiptPath = join(home, ".copilot", "local-agent-runs", "qualified-route.json"); const viaReceipt = resolveLocalDelegationAvailability({ env: {}, - home: "C:\\home", + home, now: Date.parse("2026-08-14T12:00:00Z"), - findSkill: () => "C:\\skills\\local-agent-delegation", + findSkill: () => skillDir, exists: (p) => p === receiptPath, readFile: () => JSON.stringify({ status: "qualified", @@ -79,9 +85,9 @@ test("availability accepts env route id or a qualified receipt", () => { const expired = resolveLocalDelegationAvailability({ env: {}, - home: "C:\\home", + home, now: Date.parse("2027-01-01T00:00:00Z"), - findSkill: () => "C:\\skills\\local-agent-delegation", + findSkill: () => skillDir, exists: (p) => p === receiptPath, readFile: () => JSON.stringify({ status: "qualified", @@ -107,13 +113,19 @@ test("requested on + unavailable stays ineffective with a warning", () => { }); test("launch env enables when effective; -i prompt stays short and quote-free", () => { - const base = { PATH: "/usr/bin", WORKSHOP_LOCAL_DELEGATION: "enabled" }; + const base = { + PATH: "/usr/bin", + WORKSHOP_LOCAL_DELEGATION: "enabled", + workshop_local_delegation: "enabled", + }; const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false }); assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false); + assert.equal(Object.hasOwn(offEnv, "workshop_local_delegation"), false); assert.equal(offEnv.PATH, "/usr/bin"); const onEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: true }); assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled"); + assert.equal(Object.hasOwn(onEnv, "workshop_local_delegation"), false); const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false }); const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true }); @@ -126,6 +138,13 @@ test("launch env enables when effective; -i prompt stays short and quote-free", assert.equal(isSafeDeskOrientPrompt(onPrompt), true); assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false); assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false); + + assert.match(windowsLocalDelegationCmdPrefix(true), /enabled/); + assert.match(windowsLocalDelegationCmdPrefix(false), /WORKSHOP_LOCAL_DELEGATION="&&/); + const run = ["C:\\tools\\copilot.exe", "-i", onPrompt]; + const cmdLine = windowsLocalDelegationCmdPrefix(true) + run.map(quoteWindowsCmdArgument).join(" "); + assert.match(cmdLine, /^set "WORKSHOP_LOCAL_DELEGATION=enabled"&& /); + assert.match(cmdLine, /copilot\.exe/); }); test("open notice reports effective route without savings claims", () => { From c24c319880351a330571a84956f3ae8ffab515b9 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:35:26 -0700 Subject: [PATCH 6/9] fix: store Local Delegation preference user-locally, not in the workshop repo A cloned workshop must not be able to ship preference:on. Key operator intent under ~/.copilot/workshop-local-delegation/ by canonical workshop path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/README.md | 4 ++- extensions/signals-dashboard/extension.mjs | 30 ++++++++++++------- .../signals-dashboard/local-delegation.mjs | 24 +++++++++++++++ .../local-delegation.test.mjs | 26 +++++++++++++++- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/extensions/signals-dashboard/README.md b/extensions/signals-dashboard/README.md index 7f5a56d88..c90b4ab8c 100644 --- a/extensions/signals-dashboard/README.md +++ b/extensions/signals-dashboard/README.md @@ -107,7 +107,9 @@ Availability is fail-closed. Cairn enables the lane only when: receipt at `~/.copilot/local-agent-runs/qualified-route.json` (`status: "qualified"`, safe `route_id`). -Operator preference is stored at the workshop root in `.local-delegation.json`. +Operator preference is stored **user-locally** under +`~/.copilot/workshop-local-delegation/` (keyed by the canonical workshop path), +never in the cloned workshop — a repo cannot ship `preference: on`. When the preference is on but availability fails, opens still launch as frontier desks and surface the reason — they never silently fall back with savings credit. diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index c22d752fc..1e8254b6a 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -5,8 +5,8 @@ import { createServer } from "node:http"; import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs"; -import { readdir, readFile, writeFile, stat, rename, unlink } from "node:fs/promises"; -import { join, delimiter, isAbsolute, sep } from "node:path"; +import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises"; +import { join, delimiter, isAbsolute, sep, dirname } from "node:path"; import { spawn } from "node:child_process"; import { randomBytes } from "node:crypto"; import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; @@ -20,10 +20,10 @@ import { quoteWindowsCmdArgument, } from "./launch-profile.mjs"; import { - LOCAL_DELEGATION_STATE_FILE, buildLocalDelegationLaunchEnv, deskOrientPrompt, formatLocalDelegationOpenNotice, + localDelegationPreferencePath, normalizeLocalDelegationPreference, parseLocalDelegationState, resolveLocalDelegationAvailability, @@ -365,9 +365,19 @@ function isInsideRoot(root, target) { } catch { return false; } } +function preferenceStatePath(workshopDir) { + return localDelegationPreferencePath(workshopDir, { + resolvePath: (p) => { + try { return realpathSync(p); } catch { return p; } + }, + }); +} + async function readLocalDelegationPreference(workshopDir) { + // Never read preference from the workshop repo — a clone can ship + // preference:on. Only user-local state (or explicit env) counts. try { - const raw = JSON.parse(await readFile(join(workshopDir, LOCAL_DELEGATION_STATE_FILE), "utf8")); + const raw = JSON.parse(await readFile(preferenceStatePath(workshopDir), "utf8")); return parseLocalDelegationState(raw).preference; } catch { return normalizeLocalDelegationPreference( @@ -377,20 +387,18 @@ async function readLocalDelegationPreference(workshopDir) { async function writeLocalDelegationPreference(workshopDir, preference) { const state = serializeLocalDelegationState({ preference }); - const target = join(workshopDir, LOCAL_DELEGATION_STATE_FILE); - // Write via a same-directory temp file + rename so a checked-in symlink at - // .local-delegation.json cannot redirect the write outside the workshop. + const target = preferenceStatePath(workshopDir); + await mkdir(dirname(target), { recursive: true }); + // Atomic replace in the user-local dir (temp + rename). const tmp = join( - workshopDir, - `.local-delegation.${process.pid}.${randomBytes(4).toString("hex")}.tmp`); + dirname(target), + `.pref.${process.pid}.${randomBytes(4).toString("hex")}.tmp`); const body = JSON.stringify(state, null, 2) + "\n"; try { await writeFile(tmp, body, { encoding: "utf8", flag: "wx" }); try { await rename(tmp, target); } catch { - // Windows may refuse rename-over-existing; replace without following - // a dangling external target by unlinking the name first. await unlink(target).catch(() => {}); await rename(tmp, target); } diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index 759ff0fed..26ce6507d 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -5,9 +6,32 @@ import { join } from "node:path"; const PREFERENCES = new Set(["off", "on"]); const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +/** @deprecated Repo-root state is rejected; kept only for docs/migration mentions. */ export const LOCAL_DELEGATION_STATE_FILE = ".local-delegation.json"; export const LOCAL_DELEGATION_ENV = "WORKSHOP_LOCAL_DELEGATION"; export const LOCAL_DELEGATION_SKILL_NAME = "local-agent-delegation"; +export const LOCAL_DELEGATION_USER_STATE_DIR = join(".copilot", "workshop-local-delegation"); + +/** + * Stable user-local preference path for a workshop root. + * Permission state must NOT live in the cloned workshop (a repo can ship + * preference:on). Key by a hash of the canonical workshop path under ~/.copilot. + */ +export function localDelegationPreferencePath(workshopDir, { + home = homedir(), + resolvePath = (p) => p, +} = {}) { + if (typeof workshopDir !== "string" || !workshopDir) { + throw new Error("workshopDir is required"); + } + let canonical = workshopDir; + try { canonical = resolvePath(workshopDir); } catch { /* keep input */ } + const key = createHash("sha256") + .update(String(canonical).replaceAll("\\", "/").toLowerCase()) + .digest("hex") + .slice(0, 32); + return join(home, LOCAL_DELEGATION_USER_STATE_DIR, `${key}.json`); +} export function isLocalDelegationPreference(value) { return typeof value === "string" && PREFERENCES.has(value.toLowerCase()); diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index d066c9e67..fdf31c296 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { join } from "node:path"; +import { join, sep } from "node:path"; import { LOCAL_DELEGATION_ORIENT_LINE, buildLocalDelegationLaunchEnv, @@ -9,6 +9,7 @@ import { formatLocalDelegationOpenNotice, isLocalDelegationPreference, isSafeDeskOrientPrompt, + localDelegationPreferencePath, localSavingsCredit, normalizeLocalDelegationPreference, parseLocalDelegationState, @@ -217,6 +218,29 @@ test("state parse/serialize defaults to off", () => { assert.equal(typeof serialized.updatedAt, "string"); }); +test("preference path is user-local and keyed by workshop path, not the repo root", () => { + const home = join("user-home"); + const a = localDelegationPreferencePath(join("repos", "workshop-a"), { + home, + resolvePath: (p) => p, + }); + const b = localDelegationPreferencePath(join("repos", "workshop-b"), { + home, + resolvePath: (p) => p, + }); + const again = localDelegationPreferencePath(join("repos", "workshop-a"), { + home, + resolvePath: (p) => p, + }); + assert.match(a, /workshop-local-delegation/); + assert.equal(a.startsWith(home), true); + assert.equal(a.includes(`${sep}repos${sep}`), false); + assert.notEqual(a, b); + assert.equal(a, again); + // Repo-shipped .local-delegation.json is never the preference path. + assert.equal(a.endsWith(".local-delegation.json"), false); +}); + test("skill discovery respects explicit dir and common install roots", () => { const found = findLocalDelegationSkillDir({ env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\sealed\\.github\\skills\\local-agent-delegation" }, From 513aee9ec776cfa37bfd244f95e942f6e3ffd6d4 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:42:17 -0700 Subject: [PATCH 7/9] fix: preserve path case in Local Delegation preference key Lowercasing collided distinct workshops on case-sensitive filesystems. Normalize separators only; realpath already supplies a stable path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/local-delegation.mjs | 4 +++- .../signals-dashboard/local-delegation.test.mjs | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/signals-dashboard/local-delegation.mjs b/extensions/signals-dashboard/local-delegation.mjs index 26ce6507d..3be0bc30a 100644 --- a/extensions/signals-dashboard/local-delegation.mjs +++ b/extensions/signals-dashboard/local-delegation.mjs @@ -26,8 +26,10 @@ export function localDelegationPreferencePath(workshopDir, { } let canonical = workshopDir; try { canonical = resolvePath(workshopDir); } catch { /* keep input */ } + // Normalize separators only. Do not lowercase: on case-sensitive filesystems + // /work/Foo and /work/foo are distinct workshops and must not share state. const key = createHash("sha256") - .update(String(canonical).replaceAll("\\", "/").toLowerCase()) + .update(String(canonical).replaceAll("\\", "/")) .digest("hex") .slice(0, 32); return join(home, LOCAL_DELEGATION_USER_STATE_DIR, `${key}.json`); diff --git a/extensions/signals-dashboard/local-delegation.test.mjs b/extensions/signals-dashboard/local-delegation.test.mjs index fdf31c296..d0962ecff 100644 --- a/extensions/signals-dashboard/local-delegation.test.mjs +++ b/extensions/signals-dashboard/local-delegation.test.mjs @@ -239,6 +239,17 @@ test("preference path is user-local and keyed by workshop path, not the repo roo assert.equal(a, again); // Repo-shipped .local-delegation.json is never the preference path. assert.equal(a.endsWith(".local-delegation.json"), false); + + // Case-sensitive filesystems: Foo and foo must not share permission state. + const upper = localDelegationPreferencePath("/work/Foo", { + home, + resolvePath: (p) => p, + }); + const lower = localDelegationPreferencePath("/work/foo", { + home, + resolvePath: (p) => p, + }); + assert.notEqual(upper, lower); }); test("skill discovery respects explicit dir and common install roots", () => { From c96af104b188ae368e5a44117358d35a0c405b04 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:49:08 -0700 Subject: [PATCH 8/9] fix: allow parentheses in quoted Windows Local Delegation launches Args are quote-wrapped; only block % and ! expanders inside quotes so paths like C:\Work\Project (1) still launch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/extension.mjs | 1 + extensions/signals-dashboard/launch-profile.mjs | 10 ++++++++++ extensions/signals-dashboard/launch-profile.test.mjs | 1 + 3 files changed, 12 insertions(+) diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index 1e8254b6a..b8e92988c 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -13,6 +13,7 @@ import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; import { buildDeskAgentArgv, isDeskProfile, + isSafeQuotedWindowsCmdArg, isSafeWindowsCmdShim, isWindowsAppExecutionAlias, normalizeDeskProfile, diff --git a/extensions/signals-dashboard/launch-profile.mjs b/extensions/signals-dashboard/launch-profile.mjs index 4f06c6f28..eeb4084fb 100644 --- a/extensions/signals-dashboard/launch-profile.mjs +++ b/extensions/signals-dashboard/launch-profile.mjs @@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(value) { return typeof value === "string" && !/[%\r\n]/.test(value); } +/** + * Args that will be wrapped with quoteWindowsCmdArgument. Inside double quotes, + * cmd still expands %VAR% and !VAR! (delayed expansion). Other metacharacters + * like & | < > ^ ( ) are literal when quoted, so paths such as + * C:\Work\Project (1) must be allowed. + */ +export function isSafeQuotedWindowsCmdArg(value) { + return typeof value === "string" && !/[%!\r\n]/.test(value); +} + export function parsePluginMcpNames(text) { let parsed; try { parsed = JSON.parse(text); } diff --git a/extensions/signals-dashboard/launch-profile.test.mjs b/extensions/signals-dashboard/launch-profile.test.mjs index bde1f7303..5d28c0d4e 100644 --- a/extensions/signals-dashboard/launch-profile.test.mjs +++ b/extensions/signals-dashboard/launch-profile.test.mjs @@ -7,6 +7,7 @@ import { join } from "node:path"; import { buildDeskAgentArgv, isDeskProfile, + isSafeQuotedWindowsCmdArg, isSafeWindowsCmdShim, isWindowsAppExecutionAlias, normalizeDeskProfile, From b909b6e7c3ffd614fcf6ef848252e09b2648e310 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Fri, 14 Aug 2026 12:50:57 -0700 Subject: [PATCH 9/9] fix: apply quoted-arg Windows safety guard in launch path Complete the parentheses fix: use isSafeQuotedWindowsCmdArg for cmdSafe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750 --- extensions/signals-dashboard/extension.mjs | 5 +++-- extensions/signals-dashboard/launch-profile.test.mjs | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index b8e92988c..5a1d597e7 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -443,8 +443,9 @@ async function launchDeskConsole( // wt.exe does not reliably forward Node's spawn env into a new tab when // Windows Terminal is already running. Always start through cmd.exe and // set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself. - const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)) - && run.every((arg) => isSafeWindowsCmdShim(arg)); + // Args are quoteWindowsCmdArgument'd, so only block expanders that still + // fire inside quotes (% and !) — allow parentheses in workshop paths. + const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg)); if (!cmdSafe || !cmd) return false; const inner = windowsLocalDelegationCmdPrefix(effective) + run.map(quoteWindowsCmdArgument).join(" "); diff --git a/extensions/signals-dashboard/launch-profile.test.mjs b/extensions/signals-dashboard/launch-profile.test.mjs index 5d28c0d4e..c99df844d 100644 --- a/extensions/signals-dashboard/launch-profile.test.mjs +++ b/extensions/signals-dashboard/launch-profile.test.mjs @@ -42,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () = assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\""); assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true); assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false); + // Quoted args may contain parentheses (common workshop folders). + assert.equal(isSafeQuotedWindowsCmdArg("C:\\Work\\Project (1)"), true); + assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\%USERNAME%\\w"), false); + assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\!DELAY!\\w"), false); }); test("executes a Windows cmd shim with safe quoting", {