feat: add fail-closed Local Delegation to Cairn (signals-dashboard 0.3.0) - #2666
feat: add fail-closed Local Delegation to Cairn (signals-dashboard 0.3.0)#2666jennyf19 wants to merge 10 commits into
Conversation
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
🔒 PR Risk Scan ResultsScanned 11 changed file(s).
|
There was a problem hiding this comment.
Pull request overview
Ports fail-closed Local Delegation into Cairn while keeping repo/connected profiles independent.
Changes:
- Adds delegation availability, state, launch environment, UI, and API handling.
- Adds unit tests and extension documentation.
- Bumps Signals Dashboard to v0.3.0 and updates marketplace metadata.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
plugins/the-workshop/README.md |
Documents the Local toggle. |
plugins/signals-dashboard/plugin.json |
Updates metadata and version. |
extensions/signals-dashboard/README.md |
Documents Cairn and delegation behavior. |
extensions/signals-dashboard/package.json |
Bumps version and adds tests. |
extensions/signals-dashboard/local-delegation.test.mjs |
Tests delegation behavior. |
extensions/signals-dashboard/local-delegation.mjs |
Implements availability and launch state. |
extensions/signals-dashboard/extension.mjs |
Integrates delegation into Cairn. |
.github/plugin/marketplace.json |
Publishes v0.3.0 metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- 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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
extensions/signals-dashboard/extension.mjs:801
- This is an on/off toggle, but its accessible name only describes the next action and does not expose the current pressed state. Add
aria-pressedbased on the stored preference so screen-reader users can determine whether Local Delegation is selected, including the requested-but-unavailable state.
<button data-act="local-delegation" data-preference="${esc(next)}"
aria-label="Set Local Delegation ${esc(next)}"
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
extensions/signals-dashboard/local-delegation.mjs:54
- Windows environment variable names are case-insensitive, but spreading
process.envcreates a normal case-sensitive object. Deleting only the canonical spelling leaves a parent key such asworkshop_local_delegation=enabled, so an off launch can still inherit delegation as enabled. Remove all case-insensitive matches before optionally adding the canonical key.
delete env[LOCAL_DELEGATION_ENV];
extensions/signals-dashboard/extension.mjs:416
- On Windows,
wt.exedoes not pass the caller's process environment to the command created in a new tab (especially when Terminal is already running). Bothwtbranches therefore report Local Delegation as effective while the launched Copilot process may never receiveWORKSHOP_LOCAL_DELEGATION=enabled; an off launch can likewise inherit a stale value from the Terminal broker. The terminal command needs to set or clear the variable explicitly before starting the agent, with coverage for both states.
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run], { env })) return true;
extensions/signals-dashboard/local-delegation.mjs:44
- This intentionally makes the effective-on and off prompts identical, contradicting the PR summary's promised frontier-owned orientation note and the linked Sealed Delegation launch contract, which says the desk instruction should establish the authority boundary. The installed skill does not consume this environment flag itself, so the toggle currently changes no frontier instruction. Add a short shell-safe instruction (or another enforced handoff) when effective, or revise the stated contract if env-only behavior is intended.
This issue also appears on line 54 of the same file.
// localDelegationEffective is intentionally unused in the -i string: policy
// is carried by env + skill so Windows launch cannot reparse a long appendix.
void localDelegationEffective;
Keep -i short with one optional ASCII line. Show operators "Local Delegation effective · route <id>" on open and in the summary control. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c39b7696-c854-40c2-b2d6-cce84ba09750
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
extensions/signals-dashboard/extension.mjs:417
- When
wt.exeattaches to an already-running Windows Terminal instance, the new tab inherits the Terminal server's environment rather than the environment passed to thiswt.execlient. Consequently an effective launch can omitWORKSHOP_LOCAL_DELEGATION, while an off launch can retain a stale enabled value, despite Cairn reporting the requested state. Set or clear the variable inside the command executed in the new tab (for both direct and cmd-shim branches), rather than relying onspawn(..., { env }).
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run], { env })) return true;
extensions/signals-dashboard/extension.mjs:387
- This recomputes availability synchronously on every render/open. The dashboard refreshes
/every five seconds, and the common unavailable case makesfindLocalDelegationSkillDirsynchronously traverse every installed marketplace/plugin and probe up to four paths per plugin each time, blocking the server event loop and creating continuous filesystem churn. Cache this probe for a bounded TTL (as MCP discovery already does) or make discovery asynchronous, with an explicit refresh/invalidation policy.
const availability = resolveLocalDelegationAvailability();
extensions/signals-dashboard/extension.mjs:805
- This is a stateful toggle, but its accessible name exposes only the next action and no programmatic pressed state; screen-reader users cannot distinguish Off, requested-On-but-unavailable, and effective-On, since
titleis not a reliable accessible description. Expose the preference viaaria-pressedand include the effective/availability text in the accessible name or description.
<button data-act="local-delegation" data-preference="${esc(next)}"
aria-label="Set Local Delegation ${esc(next)}"
style="background:#020617;border:1px solid ${border};color:${color};padding:2px 8px;border-radius:999px;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
extensions/signals-dashboard/extension.mjs:382
writeFilefollows an existing.local-delegation.jsonsymlink. A workshop repository can therefore contain a planted symlink, and clicking the new toggle will truncate and overwrite an arbitrary writable file outside the workshop. Write to a newly created temporary file in the workshop and atomically rename it over the state path (with cleanup/concurrency handling), so the destination symlink itself is replaced rather than followed.
await writeFile(
join(workshopDir, LOCAL_DELEGATION_STATE_FILE),
JSON.stringify(state, null, 2) + "\n",
"utf8");
extensions/signals-dashboard/extension.mjs:1161
- When terminal launch fails and Local Delegation was requested but unavailable,
localDetailreplacespath. If clipboard access also fails, the toast says “copy this path” but never displays the path, removing the only fallback handle. Keep the path in the detail and append the delegation warning instead of replacing it.
showToast(copyTitle + localTitle, localDetail || path);
extensions/signals-dashboard/README.md:110
- The linked source implementation also adds
.local-delegation.jsonto the workshop.gitignore, but this port writes the runtime preference at the repository root without adding an equivalent ignore rule to the Workshop scaffolding. Toggling Local in a Git-backed workshop created by this plugin will leave operator runtime state untracked and easy to commit accidentally. Update the workshop creation guidance/scaffold (and existing-workshop guidance) to ignore this file.
Operator preference is stored at the workshop root in `.local-delegation.json`.
- 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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
extensions/signals-dashboard/extension.mjs:439
- This safety check also examines the
--add-dirworkshop path and resolved executable path, not just untrusted desk input. Valid Windows locations such asC:\Work (Local)orC:\R&D\Workshoptherefore makecmdSafefalse and every desk open falls back instead of launching. Preserve support for quoted metacharacters in trusted paths while still preventing cmd expansion/injection.
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg))
&& run.every((arg) => isSafeWindowsCmdShim(arg));
if (!cmdSafe || !cmd) return false;
extensions/signals-dashboard/extension.mjs:1180
- When a terminal launch fails while Local Delegation was requested but unavailable,
localDetailcontains the availability reason and replacespath. The toast then says “copy this path” but never displays the path, defeating the manual fallback when clipboard access also failed. Include both the path and the Local Delegation detail in this branch.
const copyTitle = copied ? (name + ' · path copied') : (name + ' · copy this path');
showToast(copyTitle + localTitle, localDetail || path);
…hop 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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
extensions/signals-dashboard/extension.mjs:1517
- This description still says the toggle defaults from
.local-delegation.json, but the implementation now deliberately ignores repo-root state and reads the hashed user-local file under~/.copilot/workshop-local-delegation/. Update the action contract so callers are not directed to a rejected state file.
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
extensions/signals-dashboard/local-delegation.mjs:207
- The dashboard refreshes every five seconds, and each render recomputes availability, so when the skill is absent this synchronous scan walks every installed marketplace and plugin and probes four paths per plugin on every refresh. That blocks the loopback server's event loop and scales with the user's entire plugin inventory. Cache skill discovery (including a negative result) for a bounded TTL while continuing to re-check the lightweight route receipt.
if (exists(pluginsRoot)) {
try {
for (const market of readdirSync(pluginsRoot, { withFileTypes: true })) {
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
extensions/signals-dashboard/extension.mjs:1517
- This tool description still says the toggle defaults from
.local-delegation.json, but this change deliberately ignores repository-root state and stores the preference under~/.copilot/workshop-local-delegation/. The stale schema text can direct callers to the rejected file and contradicts the new fail-closed storage model.
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
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
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
extensions/signals-dashboard/extension.mjs:1518
- This API description points consumers to
.local-delegation.json, but the implementation now deliberately ignores repo-root state and reads the hashed user-local preference under~/.copilot/workshop-local-delegation/. Update the schema text so agents do not create or rely on a file that has no effect.
enum: ["off", "on"],
extensions/signals-dashboard/extension.mjs:831
- When the preference is on but availability fails, the accessible name becomes
Local Delegation On*; the asterisk has no accessible explanation and incorrectly sounds effective to screen-reader users. Include the requested-but-unavailable state and reason in the button's accessible name.
<button data-act="local-delegation" data-preference="${esc(next)}"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
extensions/signals-dashboard/extension.mjs:1519
- This tool description still says the default comes from
.local-delegation.json, but the implementation now explicitly ignores repo-root state and reads the user-local hashed preference under~/.copilot/workshop-local-delegation/. Since this schema is exposed to the agent, it can direct callers toward a file that has no effect and contradicts the fail-closed storage model. Describe the user-local Cairn preference instead.
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
Summary
Ports the Local Delegation seam from jennyf19/the-workshop#18 into the awesome-copilot
signals-dashboardcanvas (v0.3.0) and Workshop plugin docs.repo/connecteddesk profileslocal-agent-delegationskill and qualified route receipt /WORKSHOP_LOCAL_DELEGATION_ROUTE_IDWORKSHOP_LOCAL_DELEGATION=enabledand appends a frontier-owned orientation noteRuntime/launcher/gates remain in sealed-delegation. Cairn owns only toggle, availability, launch instruction, and visible state.
Test plan
node --test extensions/signals-dashboard/launch-profile.test.mjs extensions/signals-dashboard/local-delegation.test.mjs(17/17)node eng/generate-marketplace.mjs