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
21 changes: 17 additions & 4 deletions .agents/skills/launch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ You're working on VS Code itself and you want to:

This skill provides a launcher that clones an authenticated user-data-dir to a throwaway temp folder, picks free ports for every debug surface, and prints them as JSON so you can pick them up programmatically.

The clone is **slim**: workspace storage, browser caches, file history, cached VSIX backups, and old logs are excluded by default. On macOS, auth tokens live in the OS keychain plus small files inside `User/globalStorage` - both of which *are* preserved.
The clone is **slim**: workspace storage, browser caches, file history, cached VSIX backups, and old logs are excluded by default. On macOS, auth tokens live in the OS keychain plus small files inside `User/globalStorage` - both of which *are* preserved. On Windows the GitHub session lives in the **shared-data-dir** instead, which the launcher seeds separately (see [Windows authentication](#windows-authentication)).

## Prerequisites

Expand Down Expand Up @@ -74,7 +74,20 @@ The exclude list mirrors the one used by VS Code's own perf-test skill (`.github

#### Windows authentication

Windows has no shared per-app keychain for these secrets. They live in the copied profile, notably `User/globalStorage/state.vscdb` and root `Local State`, so the launcher verifies that they (plus `machineid` and `Network`) survived the copy. If a launched instance prompts for sign-in, launch `.\scripts\code.bat --user-data-dir=<source-udd>` directly, sign in once, and close it; every later launch copies that source profile and inherits the session.
Windows has no shared per-app keychain for these secrets, so they live in files on disk - but **not all in the user-data-dir**. The GitHub session is stored at `StorageScope.APPLICATION_SHARED` *only on Windows* (see `useSharedStorage` and `CROSS_APP_SHARED_SECRET_KEYS` in `src/vs/platform/secrets/common/secrets.ts`), which puts the two halves of the credential in **different directories**:

| Piece | Location |
|---|---|
| Encrypted GitHub session blob | `<shared-data-dir>/sharedStorage/state.vscdb` |
| DPAPI-wrapped decryption key (`os_crypt.encrypted_key`) | `<user-data-dir>/Local State` |

The launcher therefore seeds **both**: it copies the source profile *and* copies the source shared-data-dir into the run's throwaway `shared-data` dir. The source resolves the same way `IEnvironmentService.appSharedDataHome` does - `$env:CODE_OSS_DEV_AUTHED_SHARED_DATA_DIR` if set, else `$env:VSCODE_PORTABLE\shared-data` when running portable, else `~/<product.sharedDataFolderName>` (i.e. `%USERPROFILE%\.vscode-oss-shared`). It also verifies `Local State`, `machineid`, and `Network` survived the profile copy, and warns on stderr if neither database holds a GitHub session.

> This asymmetry is invisible on macOS/Linux, where the same token lands inside the profile. A Windows-only "always signed out" symptom is a shared-data-dir problem, **not** a profile problem: signing in against the source profile writes a perfectly good session, but before this seeding existed every launch handed Code OSS an empty shared dir and threw it away.

To (re)establish the source session: run `.\scripts\code.bat --user-data-dir=$env:USERPROFILE\.vscode-oss-dev` directly, sign in once, and close it. That writes the blob to `%USERPROFILE%\.vscode-oss-shared` and the key to the profile's `Local State`; later launches copy both and inherit the session.

> Profiles that predate the `APPLICATION_SHARED` migration can still hold the secret in `User/globalStorage/state.vscdb`. `ApplicationSharedStorageMain` registers application storage as a read fallback, so those profiles authenticate even with no shared-data-dir present - which is why a missing shared dir is reported as a fact rather than assumed fatal.

Excluded (transient, regenerable, or known-not-needed):
- `User/workspaceStorage/` - per-workspace state, **including stored chat sessions** (often multi-GB)
Expand Down Expand Up @@ -325,7 +338,7 @@ You can run `@playwright/cli` and `dap-cli` against the **same window simultaneo

Every launch picks fresh ports and a fresh temp `runDir`, so you can run as many concurrent Code OSS windows as your machine can handle. Each one's ports come back in its own JSON blob - keep them separate.

The launcher also passes `--shared-data-dir=<runDir>/shared-data`. This is **required** for multi-instance isolation: Code OSS keeps a fixed-path SQLite DB at `~/.<dataFolderName>-shared/sharedStorage/state.vscdb` that is *not* covered by `--user-data-dir`. Without overriding it, two concurrent instances would fight over the same file and one would die with "shared background process terminated unexpectedly". Each launch gets its own `shared-data` dir.
The launcher also passes `--shared-data-dir=<runDir>/shared-data`. This is **required** for multi-instance isolation: Code OSS keeps a fixed-path SQLite DB at `~/.<dataFolderName>-shared/sharedStorage/state.vscdb` that is *not* covered by `--user-data-dir`. Without overriding it, two concurrent instances would fight over the same file and one would die with "shared background process terminated unexpectedly". Each launch gets its own `shared-data` dir, **seeded from the source shared-data-dir** so the Windows GitHub session survives - see [Windows authentication](#windows-authentication) for why that copy matters.

## Restart after source changes

Expand Down Expand Up @@ -374,4 +387,4 @@ Code OSS is a full Electron app and easily eats 1-4 GB. Always clean up.
- **`launch.sh` exits non-zero with a log tail** - either pre-launch failed, `code.sh` died before CDP came up, or CDP never opened within 90s. The tail printed to stderr is from `runDir/code.log` - read it to diagnose.
- **Snapshot shows the wrong page or no expected controls** - use `tab-list`, switch with `tab-select <index>` if needed, then re-snapshot before interacting.
- **CLI typing commands complete but the input stays empty** - focus chat with the platform shortcut, use `press` or clipboard paste rather than `fill` / `type`, then verify the input state before sending.
- **Auth missing in the launched window** - confirm the source profile is actually authed (`ls "$SOURCE_UDD"` should contain `User/`, and `ls "$SOURCE_UDD/User/globalStorage"` should show persisted extension state). On Windows, sign in directly against the source profile once so its copied `state.vscdb` and `Local State` contain the session.
- **Auth missing in the launched window** - confirm the source profile is actually authed (`ls "$SOURCE_UDD"` should contain `User/`, and `ls "$SOURCE_UDD/User/globalStorage"` should show persisted extension state). **On Windows, check the shared-data-dir first**: the GitHub session blob lives in `%USERPROFILE%\.vscode-oss-shared\sharedStorage\state.vscdb`, not in the profile. The launcher logs `copying shared data: <src> -> <dst>` on stderr when it finds it, and warns `no shared-data-dir at <path>` when it doesn't. A missing or empty source shared-data-dir means signing in again against the source profile is what you need - see [Windows authentication](#windows-authentication).
122 changes: 103 additions & 19 deletions .agents/skills/launch/scripts/launch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,65 @@ function Get-UsableNode([string]$repoPath) {
}

$setupMessage = "Run in PowerShell from $repoPath`: fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression; fnm use"
if ($null -eq $command) {
throw "Node.js $requiredVersion or newer is required on PATH. $setupMessage"
if ($null -ne $command) {
try {
$version = & $command.Source --version 2>$null
if ($LASTEXITCODE -ne 0 -or $version -notmatch '^v(?<version>\d+\.\d+\.\d+)') {
throw 'could not determine its version'
}
if ([version]$Matches.version -lt [version]$requiredVersion) {
throw "found $version"
}
return $command.Source
} catch {
# Fall through to fnm fallback
}
}

try {
$version = & $command.Source --version 2>$null
if ($LASTEXITCODE -ne 0 -or $version -notmatch '^v(?<version>\d+\.\d+\.\d+)') {
throw 'could not determine its version'
# Fallback: Check fnm directories (most recent first)
$fnmBase = Join-Path $env:USERPROFILE 'AppData\Local\fnm_multishells'
if (Test-Path $fnmBase) {
$fnmDirs = Get-ChildItem $fnmBase -Directory -ErrorAction SilentlyContinue | Sort-Object -Property CreationTime -Descending
foreach ($dir in $fnmDirs) {
$nodePath = Join-Path $dir.FullName 'node.exe'
if (Test-Path $nodePath) {
try {
$version = & $nodePath --version 2>$null
if ($LASTEXITCODE -eq 0 -and $version -match '^v(?<version>\d+\.\d+\.\d+)') {
if ([version]$Matches.version -ge [version]$requiredVersion) {
return $nodePath
}
}
} catch { }
}
}
if ([version]$Matches.version -lt [version]$requiredVersion) {
throw "found $version"
}

throw "Node.js $requiredVersion or newer is required on PATH. $setupMessage"
}

function Get-SourceSharedDataDir([string]$repoPath) {
if ($env:CODE_OSS_DEV_AUTHED_SHARED_DATA_DIR) {
return $env:CODE_OSS_DEV_AUTHED_SHARED_DATA_DIR
}

# Mirrors IEnvironmentService.appSharedDataHome, minus the --shared-data-dir
# branch (that one names the *destination*, not the source we copy from):
# VSCODE_PORTABLE\shared-data, else ~/<product.sharedDataFolderName>.
if ($env:VSCODE_PORTABLE) {
return Join-Path $env:VSCODE_PORTABLE 'shared-data'
}

$folderName = '.vscode-oss-shared'
$productJson = Join-Path $repoPath 'product.json'
if (Test-Path -LiteralPath $productJson -PathType Leaf) {
$product = Get-Content -LiteralPath $productJson -Raw | ConvertFrom-Json
if ($product.PSObject.Properties['sharedDataFolderName']) {
$folderName = $product.sharedDataFolderName
}
return $command.Source
} catch {
throw "Node.js $requiredVersion or newer is required on PATH ($($_.Exception.Message)). $setupMessage"
}

return Join-Path $env:USERPROFILE $folderName
}

function Get-FreePort {
Expand Down Expand Up @@ -171,14 +214,13 @@ function Assert-AuthCriticalProfileFiles([string]$destination) {
}
}

function Test-SourceHasGitHubAuthenticationSecret([string]$node, [string]$source, [string]$temporaryDb) {
$sourceDb = Join-Path $source 'User\globalStorage\state.vscdb'
if (-not (Test-Path -LiteralPath $sourceDb -PathType Leaf)) {
return $null
function Test-DbHasGitHubAuthenticationSecret([string]$node, [string]$db, [string]$temporaryDb) {
if (-not (Test-Path -LiteralPath $db -PathType Leaf)) {
return $false
}

try {
[IO.File]::Copy($sourceDb, $temporaryDb, $true)
[IO.File]::Copy($db, $temporaryDb, $true)
$script = @'
import { DatabaseSync } from 'node:sqlite';

Expand Down Expand Up @@ -206,6 +248,33 @@ try {
}
}

function Test-SourceHasGitHubAuthenticationSecret([string]$node, [string]$source, [string]$sharedSource, [string]$temporaryDb) {
# On Windows the GitHub session is APPLICATION_SHARED scoped, so it lives in
# the shared-data-dir rather than the profile - see useSharedStorage in
# src/vs/platform/secrets/common/secrets.ts. Older profiles may still hold it
# in globalStorage, and both directories get copied, so either one counts.
$databases = @(
(Join-Path $sharedSource 'sharedStorage\state.vscdb'),
(Join-Path $source 'User\globalStorage\state.vscdb')
)

$undetermined = $false
foreach ($db in $databases) {
$result = Test-DbHasGitHubAuthenticationSecret $node $db $temporaryDb
if ($result -eq $true) {
return $true
}
if ($null -eq $result) {
$undetermined = $true
}
}

if ($undetermined) {
return $null
}
return $false
}

function Get-JsoncCodeMask([string]$text) {
# Returns a same-length copy of $text with every comment span blanked out.
# Offsets are preserved so a match found in the mask can be applied to the
Expand Down Expand Up @@ -287,11 +356,11 @@ function Ensure-SimpleDialogSetting([string]$settingsFile) {

$lastBrace = $maskedText.LastIndexOf('}')
if ($lastBrace -eq -1) {
throw "settings.json has no closing brace refusing to clobber it: $settingsFile"
throw "settings.json has no closing brace - refusing to clobber it: $settingsFile"
}
$firstBrace = $maskedText.IndexOf('{')
if ($firstBrace -eq -1 -or $firstBrace -ge $lastBrace) {
throw "settings.json has no opening brace refusing to clobber it: $settingsFile"
throw "settings.json has no opening brace - refusing to clobber it: $settingsFile"
}

# Whether a leading comma is needed depends only on real content, so decide
Expand Down Expand Up @@ -452,7 +521,22 @@ try {
$logFile = Join-Path $runDir 'code.log'
New-Item -ItemType Directory -Force -Path $runDir, $sharedDataDir | Out-Null
[IO.File]::WriteAllText($logFile, '', [Text.UTF8Encoding]::new($false))
$hasGitHubAuthenticationSecret = Test-SourceHasGitHubAuthenticationSecret $node $sourceUserDataDir (Join-Path $runDir 'auth-preflight.vscdb')
$sourceSharedDataDir = Get-SourceSharedDataDir $repo
if (Test-Path -LiteralPath $sourceSharedDataDir -PathType Container) {
# On Windows the GitHub session is APPLICATION_SHARED scoped, so it lives here
# and not in the profile - see useSharedStorage in
# src/vs/platform/secrets/common/secrets.ts. Without this copy the launched
# instance always prompts for sign-in.
Write-LaunchError "[launch.ps1] copying shared data: $sourceSharedDataDir -> $sharedDataDir"
Copy-ProfileDirectory $sourceSharedDataDir $sharedDataDir $false
} else {
# Not necessarily fatal: profiles predating the APPLICATION_SHARED migration
# still hold the secret in globalStorage, and ApplicationSharedStorageMain
# falls back to application storage. State the fact and let the preflight
# below decide whether a sign-in is actually coming.
Write-LaunchError "[launch.ps1] no shared-data-dir at $sourceSharedDataDir; nothing to seed"
}
$hasGitHubAuthenticationSecret = Test-SourceHasGitHubAuthenticationSecret $node $sourceUserDataDir $sourceSharedDataDir (Join-Path $runDir 'auth-preflight.vscdb')
if ($hasGitHubAuthenticationSecret -eq $false) {
Write-LaunchError "[launch.ps1] WARNING: source profile $sourceUserDataDir has no stored GitHub session; the launched instance will prompt you to sign in."
Write-LaunchError 'To fix once and for all, launch Code OSS directly against the source profile (no copy), sign in, then close it:'
Expand Down
7 changes: 7 additions & 0 deletions .github/instructions/agentHostTesting.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ The sessions process is a portable, standalone server that multiple clients can

See the agent host protocol documentation for more details.

## Service Construction

Read `src/vs/platform/agentHost/node/serviceBootstrapping.md` before adding or
moving a node Agent Host service. It is the canonical guide for service
placement, static constructor arguments, activation, test overrides, and
disposal ownership.

## End to End Testing

You can run `node ./scripts/code-agent-host.js` to start an agent host. If you pass `--enable-mock-agent`, then the `ScriptedMockAgent` will be used.
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/pr-darwin-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,6 @@ jobs:
timeout-minutes: 40
run: bash test/smoke/scripts/run-agents-window-network-proxy.sh

- name: 🧪 Run Agents Window smoke tests through Kerberos-authenticated macOS PAC proxy
if: ${{ inputs.electron_tests && inputs.smoke_tests }}
timeout-minutes: 40
run: bash test/smoke/scripts/run-agents-window-network-proxy.sh --kerberos

- name: 🧪 Run smoke tests (Browser, Chromium)
if: ${{ inputs.browser_tests && inputs.smoke_tests }}
timeout-minutes: 20
Expand Down
Loading
Loading