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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project are documented here. The format is based on

## [Unreleased]

### Fixed

- **Super CLI: Update All Agents** now enforces workspace trust before it starts, matching the
per-agent update and launch paths instead of running update commands in an untrusted workspace.

## [1.11.1] - 2026-08-08

### Changed
Expand Down
33 changes: 33 additions & 0 deletions src/command-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,36 @@ export function executableExistsOnPath(
.filter((dir) => dir.length > 0)
.some((dir) => existsWithExt(dir.replace(/[\\/]+$/, '') + separator + executable));
}

/** Minimal UI surface the workspace-trust gate needs; injected so the gate stays unit-testable. */
export interface WorkspaceTrustPrompt {
isTrusted(): boolean;
showWarningMessage(message: string, ...actions: string[]): PromiseLike<string | undefined>;
executeCommand(command: string): PromiseLike<unknown>;
openSettings(): Promise<void>;
}

/**
* Gate for every command that runs terminal commands in the workspace. Returns true when the
* workspace is trusted; otherwise shows the standard warning, offers the trust and settings
* actions, and returns false so the caller aborts before creating a terminal or running anything.
*/
export async function confirmWorkspaceTrust(prompt: WorkspaceTrustPrompt, action: string): Promise<boolean> {
if (prompt.isTrusted()) {
return true;
}

const selection = await prompt.showWarningMessage(
`Super CLI runs terminal commands in the current workspace. Trust this workspace before ${action}.`,
'Manage Workspace Trust',
'Open Settings',
);

if (selection === 'Manage Workspace Trust') {
await prompt.executeCommand('workbench.trust.manage');
} else if (selection === 'Open Settings') {
await prompt.openSettings();
}

return false;
}
14 changes: 13 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import {
shouldOfferFavoriteAfterLaunch,
shouldOfferRatingAfterLaunch,
} from './agent-view.js';
import { executableExistsOnPath, findAgentByTerminalName, isExecutableFile } from './command-utils.js';
import {
confirmWorkspaceTrust,
executableExistsOnPath,
findAgentByTerminalName,
isExecutableFile,
} from './command-utils.js';
import { buildDoctorReport, inspectAgents, type DoctorResult } from './doctor.js';
import { resolveAgentIcon } from './icons.js';
import {
Expand All @@ -30,6 +35,7 @@ import { AgentTreeDataProvider } from './tree.js';
import {
createPendingCommandsDisposable,
createSharedUpdateTerminal,
createWorkspaceTrustPrompt,
launchAgent,
openExtensionSettings,
runAgentUpdate,
Expand Down Expand Up @@ -596,6 +602,12 @@ export function activate(context: vscode.ExtensionContext): void {
// from issuing further commands; it can't kill a command already running in the terminal, which is
// why the terminal is left open for the user to see and deal with.
const updateAllAgentsCommand = vscode.commands.registerCommand('superCli.updateAllAgents', async () => {
// Every path below runs terminal commands, so the bulk command enforces the same workspace-trust
// gate as the per-agent update and launch paths before it touches any agent.
if (!(await confirmWorkspaceTrust(createWorkspaceTrustPrompt(context), 'updating agents'))) {
return;
}

// Paired with its resolved command so the loop below never has to assert it is still defined.
const withUpdateCommand = getEffectiveAgents()
.flatMap((agent) => agent.updateCommand ? [{ agent, updateCommand: agent.updateCommand }] : []);
Expand Down
40 changes: 17 additions & 23 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
appendBoundedText,
buildExtensionSettingsQuery,
buildTerminalName,
confirmWorkspaceTrust,
isTerminalCwdAmbiguous,
resolveTerminalCwd,
shouldPromptToInstall,
type WorkspaceTrustPrompt,
} from './command-utils.js';

const SETTINGS_NAMESPACE = 'superCli';
Expand Down Expand Up @@ -167,6 +169,19 @@ export async function openExtensionSettings(context: vscode.ExtensionContext): P
await vscode.commands.executeCommand('workbench.action.openSettings', buildExtensionSettingsQuery(context.extension.id));
}

/**
* Builds the trust prompt shared by every command that runs terminal commands. `isTrusted` is read
* lazily so a trust change made after activation is always honoured.
*/
export function createWorkspaceTrustPrompt(context: vscode.ExtensionContext): WorkspaceTrustPrompt {
return {
isTrusted: () => vscode.workspace.isTrusted,
showWarningMessage: (message, ...actions) => vscode.window.showWarningMessage(message, ...actions),
executeCommand: (command) => vscode.commands.executeCommand(command),
openSettings: () => openExtensionSettings(context),
};
}

/** Resolves whether launches should use VS Code's WSL terminal. */
function resolveTerminalEnvironment(): { useWsl: boolean } {
const useWsl = vscode.workspace.getConfiguration(SETTINGS_NAMESPACE).get<boolean>('useWsl', false)
Expand Down Expand Up @@ -239,19 +254,7 @@ export async function launchAgent(
// picker for ambiguous multi-root cases — is skipped entirely in favor of this exact cwd.
cwdOverride?: vscode.Uri,
): Promise<boolean> {
if (!vscode.workspace.isTrusted) {
const selection = await vscode.window.showWarningMessage(
`Super CLI runs terminal commands in the current workspace. Trust this workspace before launching ${agent.label}.`,
'Manage Workspace Trust',
'Open Settings',
);

if (selection === 'Manage Workspace Trust') {
await vscode.commands.executeCommand('workbench.trust.manage');
} else if (selection === 'Open Settings') {
await openExtensionSettings(context);
}

if (!(await confirmWorkspaceTrust(createWorkspaceTrustPrompt(context), `launching ${agent.label}`))) {
return false;
}

Expand Down Expand Up @@ -382,16 +385,7 @@ export function runAgentUpdate(

/** Runs the agent's official update command in a dedicated terminal (without launching the agent). */
export async function updateAgent(agent: Agent, context: vscode.ExtensionContext): Promise<UpdateOutcome> {
if (!vscode.workspace.isTrusted) {
const selection = await vscode.window.showWarningMessage(
`Super CLI runs terminal commands in the current workspace. Trust this workspace before updating ${agent.label}.`,
'Manage Workspace Trust',
);

if (selection === 'Manage Workspace Trust') {
await vscode.commands.executeCommand('workbench.trust.manage');
}

if (!(await confirmWorkspaceTrust(createWorkspaceTrustPrompt(context), `updating ${agent.label}`))) {
return 'unsupported';
}

Expand Down
18 changes: 18 additions & 0 deletions test/metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,24 @@ test('per-invocation disposables never accumulate in context.subscriptions', ()
assert.match(readText('src/terminal.ts'), /onDidCloseTerminal\(\(closed\) => \{/);
});

test('bulk terminal commands stay behind the workspace trust gate', () => {
const extensionSource = readText('src/extension.ts');
const terminalSource = readText('src/terminal.ts');
const commandStart = extensionSource.indexOf("registerCommand('superCli.updateAllAgents'");
const commandEnd = extensionSource.indexOf("registerCommand('superCli.openAgentDocumentation'");
const bulkCommandBody = extensionSource.slice(commandStart, commandEnd);

// The trust check must run before the command reads agents or creates any terminal.
const trustCheck = bulkCommandBody.indexOf('confirmWorkspaceTrust(');
assert.ok(trustCheck >= 0, 'updateAllAgents must check workspace trust');
assert.ok(
trustCheck < bulkCommandBody.indexOf('getEffectiveAgents()'),
'the trust check must run before the agent list is resolved',
);
assert.match(bulkCommandBody, /createWorkspaceTrustPrompt\(context\)/);
assert.match(terminalSource, /isTrusted: \(\) => vscode\.workspace\.isTrusted/);
});

test('terminal output is captured only for the missing-command detector', () => {
const terminalSource = readText('src/terminal.ts');

Expand Down
63 changes: 63 additions & 0 deletions test/trust-gate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const test = require('node:test');
const assert = require('node:assert/strict');

const { confirmWorkspaceTrust } = require('../out/command-utils.js');

/** Builds a prompt double that records the UI calls the gate makes. */
function createPrompt({ isTrusted, selection }) {
const calls = { warnings: [], commands: [], settings: 0 };

return {
calls,
prompt: {
isTrusted: () => isTrusted,
showWarningMessage: async (message, ...actions) => {
calls.warnings.push({ message, actions });
return selection;
},
executeCommand: async (command) => {
calls.commands.push(command);
},
openSettings: async () => {
calls.settings += 1;
},
},
};
}

test('trust gate allows commands in a trusted workspace without prompting', async () => {
const { prompt, calls } = createPrompt({ isTrusted: true });

assert.equal(await confirmWorkspaceTrust(prompt, 'updating agents'), true);
assert.deepEqual(calls.warnings, []);
assert.deepEqual(calls.commands, []);
assert.equal(calls.settings, 0);
});

test('trust gate refuses to start commands in an untrusted workspace', async () => {
const { prompt, calls } = createPrompt({ isTrusted: false, selection: undefined });

assert.equal(await confirmWorkspaceTrust(prompt, 'updating agents'), false);
assert.equal(calls.warnings.length, 1);
assert.match(
calls.warnings[0].message,
/Trust this workspace before updating agents\./,
);
assert.deepEqual(calls.warnings[0].actions, ['Manage Workspace Trust', 'Open Settings']);
});

test('trust gate opens workspace trust management when asked', async () => {
const { prompt, calls } = createPrompt({ isTrusted: false, selection: 'Manage Workspace Trust' });

assert.equal(await confirmWorkspaceTrust(prompt, 'launching Claude Code'), false);
assert.deepEqual(calls.commands, ['workbench.trust.manage']);
assert.equal(calls.settings, 0);
});

test('trust gate opens extension settings when asked', async () => {
const { prompt, calls } = createPrompt({ isTrusted: false, selection: 'Open Settings' });

assert.equal(await confirmWorkspaceTrust(prompt, 'updating agents'), false);
assert.deepEqual(calls.commands, []);
assert.equal(calls.settings, 1);
});
Loading