Skip to content

Activate Python environments in Agent Host shell commands with shell init scripts - #332593

Merged
Anthony Kim (anthonykim1) merged 53 commits into
mainfrom
anthonykim1/initScriptSDK
Sep 2, 2026
Merged

Activate Python environments in Agent Host shell commands with shell init scripts#332593
Anthony Kim (anthonykim1) merged 53 commits into
mainfrom
anthonykim1/initScriptSDK

Conversation

@anthonykim1

@anthonykim1 Anthony Kim (anthonykim1) commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Resolves: #332046
Part of: #323164

  • Generate one init script per Agent Host Copilot session and register it through the SDK's shell.initScripts, so it runs before every built-in shell command.
  • Source ~/.bashrc on macOS and Linux, or the current-user PowerShell profiles on Windows, then run the activation command the Python Environments extension published for the session's folder, reading only that extension's value for the tool shell.
  • Gate on the default-off, application-scoped chat.agentHost.shellTool.initScript.enabled setting and forward it to the host as the enableShellInitScript root flag; the host applies a script only while the flag is true and the SDK shell is in use, and unregisters when either changes.
  • Publish only from the Editor Window that owns the session folder; any local window may clear. Keep the value transient and session-only, accept at most one entry of 64 KiB, and reconcile synchronously before each turn.
  • Materialize one file per session instance, grant its directory to the SDK sandbox before registration, rewrite in place on change, and delete on dispose once the SDK disconnect settles. Failures are logged and retried on the next turn.
  • Encode PowerShell activation as base64 and load each profile under Continue in its own try/catch.

Notes:

  • The forwarded flag is client-writable root config like every forwarded key. Connected Agent Host clients are trusted and can already run commands, so the setting is the user's opt-in mirrored to the host, not authorization.
  • With the SDK sandbox enabled, profile files and activation scripts outside the working directory need allowRead entries; profile setup repeats before every shell command; direct script commands may need approval while a script is registered (Allow trusted host init scripts to preserve direct-script review github/copilot-sdk#2466); the Windows E2E variant is skipped by the Copilot output oracle.

Validation

  • scripts/test.sh --grep "shell init|shellInitScript|AgentHostShellInitSynchronizer|AgentHostCopilotCliSettingsContribution|copilotCliConfig|buildSandboxConfigForSdk|agentHostSchema|configValues" — 134 passing
  • ./scripts/test-integration.sh --run src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts --grep "stale model selection|shell init script runs before the shell command" — 2 passing
  • npx eslint on the changed files — clean

Inspirations from:

The SDK built-in shell tool spawns a fresh no-rc shell per command, so the
user's Python environment and shell hooks are absent. Add the generators that
produce the init script text later sourced via ShellOptions.initScripts.

Three runtime behaviors shape the generated text: only the final exit status is
observed (so every script ends in a statement that always succeeds), bash
init-script stderr is discarded (so diagnostics go to stdout), and PowerShell
scripts are dot-sourced under $ErrorActionPreference = 'Stop' (so the body is
wrapped in try/catch/finally).

The profile snippet sources ~/.bashrc and then replays marker-delimited managed
blocks that did not take effect. Stock rc files early-return in non-interactive
shells and tools such as conda append their init block below that guard, so
sourcing alone never reaches it. Replaying recovers shell functions such as
conda activate, which are never exported and so cannot arrive through the
inherited environment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Carries generated shell init script text from the client to the agent host as
per-session config. Values are pushed rather than user-authored, so the property
is readOnly; the selected Python environment can change while a session is live,
so it is sessionMutable.

The property deliberately has no default and is not added to the defaults passed
to validateOrDefault: an absent value means "nothing to apply", which must stay
distinguishable from an explicit empty array, which clears previously applied
scripts.

readOnly keeps the property out of the session settings file but does not
suppress the generic chat-input chip, so the key is also added to
WELL_KNOWN_PICKER_PROPERTIES.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Host-generated files that the SDK shell tool must read, such as a session's
shell init scripts, live under the user data path, which no existing grant
covers. Add an optional extraReadonlyPaths parameter routed through the same
precedence sets as user-supplied paths, so an explicit denyRead still wins and
a path already granted readwrite is not downgraded.

The SDK treats init script readability as a caller obligation and fails
silently when a script cannot be read, so the doc comment records that every
producer of a session sandboxConfig must pass the same paths.

Call sites are updated in a later change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Materializes the shell init snippets configured on a session and registers
their paths through options.update, so the SDK sources them before every
built-in shell tool command.

Both producers of a session sandbox policy now grant read access to the
session's script directory. CopilotAgentSession re-pushes sandboxConfig on
every turn, so granting it only at launch would silently revoke the grant on
the first turn, and the SDK reports nothing when it cannot read an init script.

The apply lives on the session rather than the launcher so it can re-run on a
mid-session config change and on a cold resume: the runtime does not persist
its init-script list, but the host's session config values are persisted.

Paths are derived from the snippet shape, so an unchanged list skips the RPC
and changed content takes effect through the rewritten file, which the runtime
re-reads before each command. Writes are atomic where the provider supports it
so a command running during a rewrite cannot source a half-written file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the workbench half: a synchronizer that resolves what a session's shell
tool needs and publishes it as script text on the session's config, which the
agent host then materializes and registers with the SDK.

Python activation is read from the environment-variable collection the Python
Environments extension publishes, scoped to the workspace folder that owns the
session. Only that extension may supply the value, since it becomes an
executable script. On POSIX the zsh variable is accepted as a fallback for
bash: the extension emits identical text for both, and the tool shell is always
bash. Fish and cmd are never used because their syntax would not parse.

Worktree-isolated sessions resolve through their originating project, since a
worktree path is not a workspace folder and activation commands carry absolute
paths.

Config is dispatched only when the value actually changes, and an empty list is
never published to a session that never had one. Session config is shared
across windows, so an unconditional write would let two windows overwrite each
other indefinitely.

Both snippets are behind experimental settings, on by default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 25, 2026 18:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Python environment activation and shell-profile loading to Agent Host’s built-in shell tool.

Changes:

  • Adds per-session shell-init configuration and workspace synchronization.
  • Materializes scripts and integrates them with SDK sandbox/session lifecycles.
  • Adds configuration settings and comprehensive tests.
Show a summary per file
File Description
src/vs/platform/agentHost/common/agentHostSchema.ts Defines the shell-init session schema.
src/vs/platform/agentHost/common/copilotCliConfig.ts Adds shell integration setting IDs.
src/vs/platform/agentHost/common/sessionConfigKeys.ts Adds the shell-init config key.
src/vs/platform/agentHost/common/shellInitSnippets.ts Generates and validates shell snippets.
src/vs/platform/agentHost/node/copilot/copilotAgent.ts Owns materialization and cleanup.
src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Applies scripts to live SDK sessions.
src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts Grants initial sandbox access.
src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts Supports extra read-only paths.
src/vs/platform/agentHost/node/copilot/shellInitScriptMaterializer.ts Materializes and prunes scripts.
src/vs/platform/agentHost/test/common/agentHostSchema.test.ts Tests schema validation.
src/vs/platform/agentHost/test/node/copilotAgent.test.ts Tests agent lifecycle integration.
src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts Tests SDK script application.
src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts Tests launcher sandbox behavior.
src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts Tests read-only sandbox grants.
src/vs/platform/agentHost/test/node/shellInitScriptMaterializer.test.ts Tests script file lifecycle.
src/vs/platform/agentHost/test/node/shellInitSnippets.test.ts Tests generated shell behavior.
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts Hides generated configuration from UI.
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts Registers session synchronization.
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostShellInitSynchronizer.ts Resolves and publishes activation snippets.
src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts Registers experimental settings.
src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostShellInitSynchronizer.test.ts Tests workbench synchronization.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (1)

src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts:6362

  • This registration is also executed by the remote Agent Host contribution, but it does not pass the handler's connection. The synchronizer always dispatches through the ambient IAgentHostService and keys registrations only by the backend URI, so a remote session never receives this config (and the same URI on two hosts can evict the other registration). Either restrict this integration to the ambient host or carry the connection/authority through the registration and use both for dispatch and identity.
			this._shellInitRegistrations.set(sessionUri, this._shellInitSynchronizer.register({
				session: URI.parse(sessionUri),
				subscription: ref.object,
			}));
  • Files reviewed: 21/21 changed files
  • Comments generated: 8
  • Review effort level: Balanced

Comment thread src/vs/platform/agentHost/node/copilot/copilotAgent.ts Outdated
Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Outdated
Comment thread src/vs/platform/agentHost/common/shellInitSnippets.ts Outdated
Comment thread src/vs/platform/agentHost/test/node/shellInitScriptMaterializer.test.ts Outdated
Comment thread src/vs/platform/agentHost/test/common/agentHostSchema.test.ts Outdated
Collapse profile loading and Python activation into one generated init script behind one setting. Inline file ownership in CopilotAgentSession, remove the generic materializer/source/index machinery, and scope publication to local workspace windows.

Also apply live config changes serially, verify conda is a shell function before skipping hook replay, and clean up the SDK-session-scoped file on session disposal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restrict publishing to the workspace window that owns the session and skip remote hosts whose OS can differ from the renderer. Serialize live script updates, require conda to be a shell function, and add missing handler test dependencies.

Inline SDK-session-scoped file ownership so cleanup cannot use a host configuration ID, and format the schema test flagged by CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 18/18 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Outdated
Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Outdated
Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Publish synchronously before the first turn, ensure the sandbox grants access before SDK registration, and serialize final cleanup behind pending script synchronization.

Limit publication to the workspace window that owns the session and trim the shell-init contract JSDoc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 18/18 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Outdated
Build expected sandbox and init-script paths through URI.fsPath so the session tests pass on Windows as well as POSIX platforms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/vs/platform/agentHost/common/shellInitScript.ts:96

  • Using powerShellBlock for profile loading changes normal profile semantics: it forces $ErrorActionPreference = 'Stop', so an ordinarily non-terminating profile error skips the remaining setup, and the finally block then discards any $ErrorActionPreference value intentionally set by the profile. Source the profiles in a dedicated try/catch without overriding/restoring that preference; keep powerShellBlock for the activation payload.
		...powerShellBlock([
			`\tforeach ($__vscodeProfile in @($PROFILE.CurrentUserAllHosts, $PROFILE.CurrentUserCurrentHost)) {`,
			`\t\tif ($__vscodeProfile -and (Test-Path -LiteralPath $__vscodeProfile)) {`,
			`\t\t\t. $__vscodeProfile`,
			`\t\t}`,
  • Files reviewed: 18/18 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts
@anthonykim1

Copy link
Copy Markdown
Contributor Author

Also picked up the suppressed note from the last review (no inline thread was created for it): PowerShell profiles no longer run under the forced $ErrorActionPreference = 'Stop' block. They now load under Continue — matching how a real session loads profiles — with a per-profile try/catch so one broken profile can't skip the next, and without a finally that would discard a preference the profile set on purpose. The Stop-wrapped block is kept only for the activation payload. Covered by a new generator test. In c709fbb.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bef9b9c0-98f2-4b26-8b8b-18119efe7b13
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bef9b9c0-98f2-4b26-8b8b-18119efe7b13
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bef9b9c0-98f2-4b26-8b8b-18119efe7b13
Keep the generated script, the session config key, the workbench
synchronizer, and the host-side materialize, grant, register, and cleanup
path, and drop the layers that accumulated during review:

- No renderer-side acknowledgement barrier. Dispatch is ordered per
  connection and the host applies session config before it starts a turn,
  so reconcile publishes synchronously again.
- No client-type authorization for the config key. Connected clients can
  already run shell commands through the agent; readOnly plus transient
  state is the model. This also removes the Agents-window settings-save
  rejection and its carry-forward patch.
- No script revisions. One file per instance is rewritten atomically in
  place and deleted only on dispose after the SDK session disconnects, so a
  command that already captured the path can still source it.
- SDK registration failures are logged and retried on the next turn rather
  than failing the turn.

The sandbox read grant is added once a script is materialized and kept for
the instance lifetime. Windows test portability, echo suppression, transient
persistence, and base64 PowerShell payloads are unchanged.
@anthonykim1 Anthony Kim (anthonykim1) changed the title Activate selected Python environments in Copilot Agent Host shells Load shell profiles and activate the selected Python environment in Agent Host shell commands Sep 2, 2026
dispose() called this._wrapper.disconnect() before the base disposal so the
script file is removed only after the SDK session disconnects. The wrapper is
assigned midway through initializeSession, so a session that fails during
launch has none, the call throws a TypeError, and the base disposal never
runs. That leaked the session and its ShellManager in the resume tests, and
in the launcher's fallback path the TypeError replaced the SDK's model error,
which broke the stale-model E2E on every platform.

Skip the disconnect-then-cleanup chain when there is no wrapper; such a
session has no script to remove either.
…nfig path

CopilotAgentSession read shellInitSnippets through the effective config
chain, so a value placed in root config by any connected client was inherited
by every Copilot session without its own value, and the workbench setting only
governed which window published. Read the session's own value only, and apply
it only while the forwarded enableShellInitScript root flag is true, so the
user setting is enforced by the host regardless of who published the value.
The flag is forwarded next to the other Copilot CLI settings and re-applied on
root config changes, unregistering a live script when it turns off.

Also: the Agents window mounts the active session's folder into its
workspace, so folder ownership alone qualified it as a publisher; it now only
clears. Cap accepted script text at 64 KiB. Remove the generated directory on
dispose even when the SDK disconnect fails. Qualify the generator note that a
sourced profile can still terminate the shell.

The recorded E2E enables the flag through root config before the snapshotted
round and leaves the root channel so its notifications stay out of the round.
The malformed-payload check ran before the forwarded flag was read, so a
registered script survived turning the setting off if the session value had
become malformed in between. The custom terminal tool check returned early
for the same reason, leaving a dormant SDK registration when the tool was
switched on mid-session.

Compute the off state first: the flag is false, or the custom terminal tool
has replaced the SDK's built-in shell. Either clears any registration without
looking at the payload; only an enabled session validates it.
dispose() started the SDK disconnect explicitly for every Copilot session so
the script file could be removed afterwards. Sessions that never materialized
a script have nothing to remove, so they now keep the plain dispose path; the
disposing flag is still set synchronously so no later sync can run.
Resolve the one conflict in the Copilot CLI settings forwarder test: upstream
added the auto-mode tiers key and this branch added the shell init flag, so
the import, the advertised schema fixture, and the expected forwarded set now
carry both, and the dispatch count is ten.
@anthonykim1
Anthony Kim (anthonykim1) marked this pull request as ready for review September 2, 2026 04:24
@anthonykim1 Anthony Kim (anthonykim1) added this to the 1.137.0 milestone Sep 2, 2026
@anthonykim1 Anthony Kim (anthonykim1) changed the title Load shell profiles and activate the selected Python environment in Agent Host shell commands Support shell custom scripts in agent host - start with activate script Sep 2, 2026
@anthonykim1

Copy link
Copy Markdown
Contributor Author

Should enable activation if you have python-envs.terminal.autoActivationType to shellStartup :

customScriptForActivation

@anthonykim1 Anthony Kim (anthonykim1) changed the title Support shell custom scripts in agent host - start with activate script Activate Python environments in Agent Host shell commands with shell init scripts Sep 2, 2026
@anthonykim1
Anthony Kim (anthonykim1) marked this pull request as draft September 2, 2026 04:45
The key was named for an early design that published separate profile and
activation snippets. The value has held one whole script since those were
collapsed, and everything around it already says script: IShellInitScript,
createShellInitScript, isShellInitScriptList, enableShellInitScript, and the
SDK's own shell.initScripts. Rename the key, the schema property, its
localization ids, and the local variables to match.

Also say the script is client-generated where two comments called it
host-generated; the host only writes the file.
@anthonykim1
Anthony Kim (anthonykim1) marked this pull request as ready for review September 2, 2026 05:09
@anthonykim1
Anthony Kim (anthonykim1) marked this pull request as draft September 2, 2026 05:09
@anthonykim1
Anthony Kim (anthonykim1) marked this pull request as ready for review September 2, 2026 06:32
@anthonykim1
Anthony Kim (anthonykim1) merged commit 3d6f125 into main Sep 2, 2026
40 checks passed
@anthonykim1
Anthony Kim (anthonykim1) deleted the anthonykim1/initScriptSDK branch September 2, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Adopt custom init script copilot SDK API

3 participants