Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
08f2a74
Add shell init snippet generators for the SDK shell tool
anthonykim1 Aug 25, 2026
8db3720
Add the shellInitSnippets session config key
anthonykim1 Aug 25, 2026
fc843c6
Let buildSandboxConfigForSdk grant extra readonly paths
anthonykim1 Aug 25, 2026
082d21a
Apply shell init scripts to SDK built-in shell tool sessions
anthonykim1 Aug 25, 2026
2aa094f
Publish Python activation and profile scripts for agent shell commands
anthonykim1 Aug 25, 2026
2334c6c
Reduce shell init integration to one script
anthonykim1 Aug 25, 2026
8c0cafc
Address shell init review feedback
anthonykim1 Aug 25, 2026
eafb955
Address latest shell init review
anthonykim1 Aug 25, 2026
c98d674
Make shell init test paths platform-neutral
anthonykim1 Aug 25, 2026
9961426
Only the owning window clears shell init; scope script files per inst…
anthonykim1 Aug 25, 2026
c709fbb
Load pwsh profiles under Continue; prune empty shell init session dirs
anthonykim1 Aug 25, 2026
fda9bb1
Drop the conda-specific rc replay from the shell init script
anthonykim1 Aug 26, 2026
540b51f
Read only the shell-matching activation variable; add SDK shell init e2e
anthonykim1 Aug 26, 2026
33bd3e3
Merge upstream main into anthonykim1/initScriptSDK
anthonykim1 Sep 1, 2026
d7c86cf
Default Agent Host shell init scripts off
anthonykim1 Sep 1, 2026
2d87d93
Correct Agent Host shell profile sourcing
anthonykim1 Sep 1, 2026
9e2dc4b
Keep Agent Host shell init state transient
anthonykim1 Sep 1, 2026
8b19d5f
Prevent shell init config update loops
anthonykim1 Sep 1, 2026
510e5b9
Encode PowerShell shell init payloads safely
anthonykim1 Sep 1, 2026
e022009
Reject malformed shell init config safely
anthonykim1 Sep 1, 2026
47589bf
Restrict shell init config to editor clients
anthonykim1 Sep 1, 2026
a7652b6
Document shell init script approval behavior
anthonykim1 Sep 1, 2026
c24669c
Cover first-turn shell init ordering
anthonykim1 Sep 1, 2026
bd366b7
Allow clients to clear shell init config
anthonykim1 Sep 1, 2026
f56b8f4
Apply shell init config before resumed turns
anthonykim1 Sep 1, 2026
afab8cf
Run the PowerShell shell init test on Windows
anthonykim1 Sep 1, 2026
fbee6c9
Close shell init config authorization bypasses
anthonykim1 Sep 1, 2026
8df3fca
Wait for pending shell init publication
anthonykim1 Sep 1, 2026
8d3a0fd
Serialize shell init config publications
anthonykim1 Sep 1, 2026
6cf6c24
Bound shell init publication acknowledgements
anthonykim1 Sep 1, 2026
30a4600
Make shell init sandbox grants conditional
anthonykim1 Sep 1, 2026
1b0d2a2
Test shell init file and activation updates
anthonykim1 Sep 1, 2026
10a6532
Clean up in-flight shell init writes on dispose
anthonykim1 Sep 1, 2026
8122749
Clear stale shell init sandbox grants
anthonykim1 Sep 1, 2026
d4e434b
Clean up failed shell init writes
anthonykim1 Sep 1, 2026
133c07d
Finish failed shell init cleanup
anthonykim1 Sep 1, 2026
62ecf31
Preserve registered shell init revisions
anthonykim1 Sep 1, 2026
d46f5ff
Document local shell init support
anthonykim1 Sep 1, 2026
595b59c
Test shell init timeout and PowerShell profiles
anthonykim1 Sep 1, 2026
596e0d3
Fix shell init tests on Windows
anthonykim1 Sep 2, 2026
bad4391
Preserve shell init in Agents settings saves
anthonykim1 Sep 2, 2026
5018440
Retain shell init revisions until disconnect
anthonykim1 Sep 2, 2026
010c113
Fail closed on shell init preflight errors
anthonykim1 Sep 2, 2026
a96908a
Bound shell init publication to one deadline
anthonykim1 Sep 2, 2026
b4a4572
Reduce Agent Host shell init to the core integration
anthonykim1 Sep 2, 2026
4536dc3
Guard shell init cleanup when a session never initialized
anthonykim1 Sep 2, 2026
75dc467
Shorten the Agent Host shell init setting description
anthonykim1 Sep 2, 2026
0e302a6
Enforce the shell init setting on the host and close the inherited co…
anthonykim1 Sep 2, 2026
978f876
Decide the shell init off state before validating the payload
anthonykim1 Sep 2, 2026
69068f2
Chain shell init cleanup only for sessions that wrote a script
anthonykim1 Sep 2, 2026
6d26ed2
Describe the forwarded shell init flag as opt-in, not authorization
anthonykim1 Sep 2, 2026
9d05bbc
Merge upstream main into anthonykim1/initScriptSDK
anthonykim1 Sep 2, 2026
aee0d7a
Rename the shell init session config key to shellInitScripts
anthonykim1 Sep 2, 2026
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
36 changes: 36 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { IMcpServerConfiguration } from '../../mcp/common/mcpPlatformTypes.
import { TelemetryConfiguration, TelemetryLevel } from '../../telemetry/common/telemetry.js';
import { telemetryLevelToAgentHostValue } from './agentHostTelemetry.js';
import { SessionConfigKey } from './sessionConfigKeys.js';
import type { IShellInitScript } from './shellInitScript.js';
import type { SessionConfigPropertySchema, SessionConfigSchema } from './state/protocol/commands.js';
import { JsonRpcErrorCodes, ProtocolError } from './state/sessionProtocol.js';

Expand Down Expand Up @@ -299,6 +300,40 @@ const permissionsProperty = schemaProperty<IPermissionsValue>({
sessionMutable: true,
});

/**
* Scripts the client generated for this session, sourced before every built-in
* shell tool command (see `common/shellInitScript.ts`). Written by the
* workbench and consumed by the Copilot provider; `readOnly` because no user
* edits it directly, `sessionMutable` because the selected Python environment
* can change while a session is live.
*
* Deliberately has no `default`: an absent value means "nothing to apply",
* which must stay distinguishable from an explicit empty array (clear).
*/
const shellInitSnippetsProperty = schemaProperty<readonly IShellInitScript[]>({
type: 'array',
title: localize('agentHost.sessionConfig.shellInitSnippets', "Shell Init Script"),
description: localize('agentHost.sessionConfig.shellInitSnippetsDescription', "A script sourced before each built-in shell tool command."),
items: {
type: 'object',
title: localize('agentHost.sessionConfig.shellInitSnippets.item', "Shell Init Script"),
properties: {
shell: {
type: 'string',
title: localize('agentHost.sessionConfig.shellInitSnippets.shell', "Shell"),
enum: ['bash', 'powershell'],
},
script: {
type: 'string',
title: localize('agentHost.sessionConfig.shellInitSnippets.script', "Script"),
},
},
required: ['shell', 'script'],
},
readOnly: true,
sessionMutable: true,
});

/**
* Session-config properties owned by the platform itself — i.e. consumed
* by the agent host rather than by any particular agent.
Expand Down Expand Up @@ -345,6 +380,7 @@ export const platformSessionSchema = createSchema({
default: 'interactive',
sessionMutable: true,
}),
[SessionConfigKey.ShellInitSnippets]: shellInitSnippetsProperty,
});

/**
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/common/copilotCliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const CopilotCliVSCodeAssignmentContextKey = 'copilotCliVSCodeAssignmentC

export const AgentHostCustomTerminalToolEnabledSettingId = 'chat.agentHost.customTerminalTool.enabled';

/** Enable VS Code's generated init script for the SDK built-in shell tool. */
export const AgentHostShellToolInitScriptEnabledSettingId = 'chat.agentHost.shellTool.initScript.enabled';

export const AgentHostCopilotSdkLogLevelSettingId = 'chat.agentHost.copilotSdk.logLevel';

export const AgentHostOpus48PromptEnabledSettingId = 'chat.agentHost.opus48Prompt.enabled';
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/agentHost/common/sessionConfigKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const enum SessionConfigKey {
AgentMerge = 'agentMerge',
/** `'agentMerge.controller'` — host-owned Agent Merge lifecycle state. */
AgentMergeController = 'agentMerge.controller',
/** `'shellInitSnippets'` — host-generated scripts sourced before built-in shell tool commands. */
ShellInitSnippets = 'shellInitSnippets',
}

/**
Expand Down
120 changes: 120 additions & 0 deletions src/vs/platform/agentHost/common/shellInitScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export type ShellInitScriptShell = 'bash' | 'powershell';

/**
* One host-generated script sourced before every SDK built-in shell command.
* The array-valued session config carries either no script (`[]`) or this one
* script, so clearing a previously applied script is explicit.
*/
export interface IShellInitScript {
readonly shell: ShellInitScriptShell;
readonly script: string;
}

function quoteBash(value: string): string {
return `'${value.replaceAll(`'`, `'\\''`)}'`;
}

function powerShellHereString(value: string): string {
if (value.split(/\r?\n/).some(line => line.trimEnd() === `'@`)) {
throw new Error(`Cannot embed a PowerShell here-string payload containing a line equal to "'@"`);
}
return `@'\n${value}\n'@`;
}

function powerShellBlock(body: readonly string[], failureMessage: string): string[] {
return [
`$__vscodePreviousErrorActionPreference = $ErrorActionPreference`,
`try {`,
`\t$ErrorActionPreference = 'Stop'`,
...body,
`} catch {`,
`\tWrite-Output '${failureMessage.replaceAll(`'`, `''`)}'`,
`} finally {`,
`\t$ErrorActionPreference = $__vscodePreviousErrorActionPreference`,
`}`,
];
}

/**
* Creates the single script VS Code registers with the SDK shell tool.
*
* Profile loading comes first because Python activation can depend on shell
* functions such as `conda`. Bash then replays conda's managed block when a
* stock non-interactive `.bashrc` guard returned before reaching it.
*
* Every script ends successfully: the runtime reports a nonzero init-script
* status before every later command, and discards Bash init-script stderr.
*/
export function createShellInitScript(shell: ShellInitScriptShell, pythonActivation: string | undefined): IShellInitScript {
return shell === 'powershell'
? createPowerShellInitScript(pythonActivation)
: createBashInitScript(pythonActivation);
}

function createBashInitScript(pythonActivation: string | undefined): IShellInitScript {
const lines = [
`# Generated by VS Code for Agent Host shell commands.`,
`if [ -r "$HOME/.bashrc" ]; then`,
`\tif ! builtin source "$HOME/.bashrc"; then`,
`\t\tprintf '%s\\n' 'copilot shell init: loading ~/.bashrc failed; continuing.'`,
`\tfi`,
`\tif ! builtin declare -F conda >/dev/null 2>&1; then`,
`\t\t__vscode_conda_block="$(sed -n '/^# >>> conda initialize >>>$/,/^# <<< conda initialize <<<$/p' "$HOME/.bashrc" 2>/dev/null)"`,
`\t\tif [ -n "$__vscode_conda_block" ] && ! builtin eval "$__vscode_conda_block"; then`,
`\t\t\tprintf '%s\\n' 'copilot shell init: conda initialization failed; continuing.'`,
`\t\tfi`,
`\t\tbuiltin unset __vscode_conda_block`,
`\tfi`,
`fi`,
];
if (pythonActivation?.trim()) {
lines.push(
`if ! builtin eval ${quoteBash(pythonActivation)}; then`,
`\tprintf '%s\\n' 'copilot shell init: Python activation failed; continuing without the selected environment.'`,
`fi`,
);
}
lines.push(`builtin true`, ``);
return { shell: 'bash', script: lines.join('\n') };
}

function createPowerShellInitScript(pythonActivation: string | undefined): IShellInitScript {
const lines = [
`# Generated by VS Code for Agent Host shell commands.`,
...powerShellBlock([
`\tforeach ($__vscodeProfile in @($PROFILE.CurrentUserAllHosts, $PROFILE.CurrentUserCurrentHost)) {`,
`\t\tif ($__vscodeProfile -and (Test-Path -LiteralPath $__vscodeProfile)) {`,
`\t\t\t. $__vscodeProfile`,
`\t\t}`,
`\t}`,
], 'copilot shell init: loading the PowerShell profile failed; continuing.'),
];
if (pythonActivation?.trim()) {
lines.push(...powerShellBlock(
[`\tInvoke-Expression ${powerShellHereString(pythonActivation)}`],
'copilot shell init: Python activation failed; continuing without the selected environment.',
));
}
lines.push(`$global:LASTEXITCODE = 0`, ``);
return { shell: 'powershell', script: lines.join('\n') };
}

/** Validates the client-pushed list before the agent host writes it to disk. */
export function isShellInitScriptList(value: unknown): value is readonly IShellInitScript[] {
return Array.isArray(value)
&& value.length <= 1
&& value.every(entry => {
if (!entry || typeof entry !== 'object') {
return false;
}
const script = entry as Partial<IShellInitScript>;
return (script.shell === 'bash' || script.shell === 'powershell')
&& typeof script.script === 'string'
&& script.script.length > 0;
});
}
Loading
Loading