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
9 changes: 9 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ src/vs/workbench/services/extensions/common/extensionPoints.json @TylerLeonhardt
# Adding entries here lets a new .js/.cjs/.mjs file land in the repo;
# review is required to make sure TypeScript is not a better choice.
.eslint-allowed-javascript-files @alexr00 @alexdima @sbatten @TylerLeonhardt

# Agents Window architecture specifications and their routing policy.
# These files describe stable contracts and should not change for routine fixes.
/src/vs/sessions/*.md @sandy081
/src/vs/sessions/contrib/layout/browser/*.md @sandy081
/src/vs/sessions/contrib/providers/*/*.md @sandy081
/.github/instructions/sessions.instructions.md @sandy081
/.github/skills/sessions/SKILL.md @sandy081
/.github/skills/chat-customizations-editor/SKILL.md @sandy081
17 changes: 9 additions & 8 deletions .github/skills/chat-customizations-editor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ Split-view management pane for AI customization items across workspace, user, ex

## Spec

**`src/vs/sessions/AI_CUSTOMIZATIONS.md`** — always read before making changes, always update after.
**`src/vs/sessions/AI_CUSTOMIZATIONS.md`** — read for ownership and interface
contracts. Update it only when those contracts change; behavior and regressions
belong in focused tests.

## Key Folders

| Folder | What |
|--------|------|
| `src/vs/workbench/contrib/chat/common/` | `ICustomizationHarnessService`, `ISectionOverride`, `IStorageSourceFilter` — shared interfaces and filter helpers |
| `src/vs/workbench/contrib/chat/common/` | `ICustomizationHarnessService`, `ISectionOverride`, `ICustomizationItemProvider` — shared interfaces |
| `src/vs/workbench/contrib/chat/browser/aiCustomization/` | Management editor, list widgets (prompts, MCP, plugins), harness service registration |
| `src/vs/sessions/contrib/chat/browser/` | Sessions-window overrides (harness service, workspace service) |
| `src/vs/sessions/contrib/sessions/browser/` | Sessions tree view counts and toolbar |
Expand All @@ -26,26 +28,25 @@ When changing harness descriptor interfaces or factory functions, verify both co

## Key Interfaces

- **`IHarnessDescriptor`** — drives all UI behavior declaratively (hidden sections, button overrides, file filters, agent gating). See spec for full field reference.
- **`IHarnessDescriptor`** — drives harness behavior declaratively (hidden sections, button overrides, item providers, agent gating). See spec for the stable ownership contract.
- **`ISectionOverride`** — per-section button customization (command invocation, root file creation, type labels, file extensions).
- **`IStorageSourceFilter`** — controls which storage sources and user roots are visible per harness/type.
- **`IExternalCustomizationItemProvider`** / **`IExternalCustomizationItem`** — internal interfaces (in `customizationHarnessService.ts`) for extension-contributed providers that supply items directly. These mirror the proposed extension API types.
- **`ICustomizationItemProvider`** / **`ICustomizationItem`** — internal interfaces (in `customizationHarnessService.ts`) for extension-contributed providers that supply items directly. These mirror the proposed extension API types.

Principle: the UI widgets read everything from the descriptor — no harness-specific conditionals in widget code.

## Extension API (`chatSessionCustomizationProvider`)

The proposed API in `src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts` lets extensions register customization providers. Changes to `IExternalCustomizationItem` or `IExternalCustomizationItemProvider` must be kept in sync across the full chain:
The proposed API in `src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts` lets extensions register customization providers. Changes to `ICustomizationItem` or `ICustomizationItemProvider` must be kept in sync across the full chain:

| Layer | File | Type |
|-------|------|------|
| Extension API | `vscode.proposed.chatSessionCustomizationProvider.d.ts` | `ChatSessionCustomizationItem` |
| IPC DTO | `extHost.protocol.ts` | `IChatSessionCustomizationItemDto` |
| ExtHost mapping | `extHostChatAgents2.ts` | `$provideChatSessionCustomizations()` |
| MainThread mapping | `mainThreadChatAgents2.ts` | `provideChatSessionCustomizations` callback |
| Internal interface | `customizationHarnessService.ts` | `IExternalCustomizationItem` |
| Internal interface | `customizationHarnessService.ts` | `ICustomizationItem` |

When adding fields to `IExternalCustomizationItem`, update all five layers. The proposed API `.d.ts` is additive-only (new optional fields are backward-compatible and do not require a version bump).
When adding fields to `ICustomizationItem`, update all five layers. The proposed API `.d.ts` is additive-only (new optional fields are backward-compatible and do not require a version bump).

## Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,32 @@ delivery slot for the managed value.
| `projectManagedSettings(values, definitions, onWarn?)` | Keeps only declared keys whose runtime value **matches the declared type**. Undeclared keys and type mismatches are **dropped (validated, never coerced)**, with an optional warning. |
| `pickManagedSettings(nativeMdm, server, file)` | Merges the channels **per key** by precedence (native MDM → server → file): the highest-precedence channel that sets a key wins, lower channels fill in keys the higher ones leave unset, and every contribution is recorded for provenance. **The extension point when adding a new channel** — extend the `ManagedSettingsChannel` union, the `MANAGED_SETTINGS_CHANNELS` order, and this function together. |
| `managedSettingValue(key)` | Builds the standard pass-through `value` callback `policyData => policyData.managedSettings?.[key]`. Use for the common "lock to the managed value, else fall through" case (see [Declaring a managed setting](#declaring-a-managed-setting-on-a-policy)). |
| `thirdPartyAgentEnabledValue(policyData)` | Shared `value` callback for the third-party harness policies (see [Governance presence](#governance-presence-disables-the-third-party-harnesses)). |

### Governance presence disables the third-party harnesses

`IPolicyData.managedSettingsActive` is `true` when **any** channel supplies **any** managed
setting — i.e. the user is governed at all, independent of which keys were set. It is set in
`AccountPolicyService.getPolicyData` from `pickManagedSettings(...).activeSources`, and unlike
`IPolicyData.managedSettings` it is **not** projected onto the keys VS Code declares, so it also
reflects runtime-owned keys VS Code never reads.

The `Claude3PIntegration` and `Codex3PIntegration` policies both use
`thirdPartyAgentEnabledValue`, which forces its setting to `false` when the account disables
chat preview features **or** when `managedSettingsActive` is `true`. Rationale: managed settings
are composed and enforced by the Copilot runtime and never reach the Claude or Codex harnesses,
so leaving those harnesses available would hand a governed user an ungoverned path around every
control the enterprise set. This mirrors the runtime-owned `sandbox.enabled` floor retiring the
local harness (`IAgentHostEnablementService.managedSandboxEnforced`).

Invariants:

- The rule keys off **presence**, not a value, so the policies deliberately declare **no**
`managedSettings` keys — they must not be added to the native MDM watcher schema.
- `AccountPolicyService.resolvePolicyValue` probes for this presence dependence (re-evaluating
the callback with `managedSettingsActive: false`) so **Developer: Policy Diagnostics**
attributes the value to the governing channel rather than to the account.
- The `value` callback stays pure and deterministic — attribution evaluates it more than once.

### Normalization: the structured-key descriptor table

Expand Down
23 changes: 20 additions & 3 deletions .github/skills/sessions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,26 @@ that preserve these boundaries:
- Shared workbench changes represent shared capability, not Sessions-specific
policy.

Update a specification when its architecture or durable behavior changes. Do not
add implementation chronology, rejected approaches, or bug narratives to a
specification.
### Specification edit gate

Bug fixes do not update specifications when they restore an existing contract.
Before editing an authoritative specification, identify all three:

1. the existing ownership, interface, lifecycle, state-machine, persistence, or
cross-component contract that intentionally changes;
2. the implementation surfaces affected by that contract change;
3. why a regression test and a brief code comment cannot fully represent it.

If any answer is missing, leave the specification unchanged. Put concrete
behavior in a focused test, keep a non-obvious implementation constraint beside
the owning code, and preserve investigation history in the issue or pull
request.

Update a specification only when component ownership, an interface or lifecycle
contract, a state machine, persistence, or a cross-component invariant changes.
Do not update specifications for styling, copy, action placement, telemetry
fields, settings defaults, implementation algorithms, or individual bug fixes.
Those details belong in code and focused tests.

## 5. Validate proportionally

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ jobs:
mkdir -p .build/nodejs-musl
NODE_VERSION=$(grep '^target=' remote/.npmrc | cut -d '"' -f 2)
BUILD_ID=$(grep '^ms_build_id=' remote/.npmrc | cut -d '"' -f 2)
az extension add --name azure-devops --upgrade --only-show-errors
az artifacts universal download \
--organization "https://dev.azure.com/monacotools" \
--project "Monaco" \
Expand Down
9 changes: 8 additions & 1 deletion build/azure-pipelines/alpine/product-build-alpine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ jobs:
condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none'))
displayName: Setup NPM Authentication

- template: ../common/foundry-local.yml@self
parameters:
phase: prepare

- task: Docker@1
inputs:
azureSubscriptionEndpoint: vscode
Expand Down Expand Up @@ -135,7 +139,6 @@ jobs:
mkdir -p .build/nodejs-musl
NODE_VERSION=$(grep '^target=' remote/.npmrc | cut -d '"' -f 2)
BUILD_ID=$(grep '^ms_build_id=' remote/.npmrc | cut -d '"' -f 2)
az extension add --name azure-devops --upgrade --only-show-errors
az artifacts universal download \
--organization "https://dev.azure.com/monacotools" \
--project "Monaco" \
Expand Down Expand Up @@ -174,6 +177,10 @@ jobs:
displayName: Install dependencies
condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'))

- template: ../common/foundry-local.yml@self
parameters:
phase: install

- script: node build/azure-pipelines/common/checkNativeOptionalDeps.ts
displayName: Verify native optional dependency binaries

Expand Down
20 changes: 2 additions & 18 deletions build/azure-pipelines/common/disableFoundryLocalInstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'fs';
import * as path from 'path';
import { disableFoundryLocalInstall } from './foundryLocalInstall.ts';

const packageJsonPath = path.resolve(import.meta.dirname, '../../..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
dependencies?: Record<string, string>;
allowScripts?: Record<string, boolean>;
};
const allowScripts = packageJson.allowScripts;
const foundryLocalVersion = packageJson.dependencies?.['foundry-local-sdk'];
const foundryLocalKey = foundryLocalVersion ? `foundry-local-sdk@${foundryLocalVersion}` : undefined;

if (!allowScripts || !foundryLocalKey || allowScripts[foundryLocalKey] !== true) {
throw new Error('Expected an approved, pinned foundry-local-sdk install script in package.json');
}

allowScripts[foundryLocalKey] = false;
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, undefined, 2)}\n`);
console.log(`Disabled ${foundryLocalKey} install script for this CI job`);
disableFoundryLocalInstall();
18 changes: 18 additions & 0 deletions build/azure-pipelines/common/foundry-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
parameters:
- name: phase
type: string
values:
- prepare
- install

steps:
- ${{ if eq(parameters.phase, 'prepare') }}:
- task: NuGetAuthenticate@1
displayName: Setup NuGet Authentication

- script: node build/azure-pipelines/common/disableFoundryLocalInstall.ts
displayName: Disable Foundry Local Native Install

- ${{ if eq(parameters.phase, 'install') }}:
- script: node build/azure-pipelines/common/foundryLocalInstall.ts
displayName: Install Foundry Local Native Dependencies
121 changes: 121 additions & 0 deletions build/azure-pipelines/common/foundryLocalInstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { execFileSync } from 'child_process';
import { createHash } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { fetchCoreLibraries, getStandardArtifacts, type IFoundryDependencyVersions, requiredCoreLibraryNames, supportsCoreLibraryTarget, VSCODE_NUGET_FEED } from '../../dictation-runtime/nuget.ts';

const repositoryRoot = path.resolve(import.meta.dirname, '../../..');
const packageName = 'foundry-local-sdk';
const credentialTokenEnvironmentVariable = 'VSS_NUGET_ACCESSTOKEN';
const expectedInstallerUtilsHash = '0831c932b10389283e805f88a204b0f6a5a8053f2ee520e56a0f0adf1352aa8b';

type RootPackageJson = {
dependencies?: Record<string, string>;
allowScripts?: Record<string, boolean>;
};

type FoundryPackageJson = {
version?: string;
scripts?: Record<string, string>;
};

function readJson<T>(filePath: string): T {
return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
}

function getPinnedPackage(root: string): { packageJsonPath: string; packageJson: RootPackageJson; allowScripts: Record<string, boolean>; allowScriptsKey: string } {
const packageJsonPath = path.join(root, 'package.json');
const packageJson = readJson<RootPackageJson>(packageJsonPath);
const allowScripts = packageJson.allowScripts;
const version = packageJson.dependencies?.[packageName];
const allowScriptsKey = version ? `${packageName}@${version}` : undefined;

if (!allowScripts || !version || !allowScriptsKey || allowScripts[allowScriptsKey] !== true) {
throw new Error(`Expected an approved, pinned ${packageName} install script in package.json`);
}

return { packageJsonPath, packageJson, allowScripts, allowScriptsKey };
}

export function disableFoundryLocalInstall(root = repositoryRoot): void {
const { packageJsonPath, packageJson, allowScripts, allowScriptsKey } = getPinnedPackage(root);
allowScripts[allowScriptsKey] = false;
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, undefined, 2)}\n`);
console.log(`Disabled ${allowScriptsKey} install script for this CI job`);
}

function validateInstallerUtils(installerUtilsPath: string): void {
const contents = fs.readFileSync(installerUtilsPath);
const actualHash = createHash('sha256').update(contents).digest('hex');

if (actualHash !== expectedInstallerUtilsHash) {
throw new Error(`Unexpected ${packageName} installer utility hash ${actualHash}`);
}
}

function runLifecycleScript(packageRoot: string, relativeScriptPath: string): void {
execFileSync(process.execPath, [path.join(packageRoot, relativeScriptPath)], {
cwd: packageRoot,
stdio: 'inherit'
});
}

export async function installFoundryLocal(root = repositoryRoot): Promise<void> {
if (!process.env[credentialTokenEnvironmentVariable]) {
throw new Error(`${credentialTokenEnvironmentVariable} was not set by NuGetAuthenticate`);
}

const rootPackageJson = readJson<RootPackageJson>(path.join(root, 'package.json'));
const version = rootPackageJson.dependencies?.[packageName];
const allowScriptsKey = version ? `${packageName}@${version}` : undefined;
if (!version || !allowScriptsKey || rootPackageJson.allowScripts?.[allowScriptsKey] !== false) {
throw new Error(`Expected the pinned ${packageName} install script to be disabled before installation`);
}

const packageRoot = path.join(root, 'node_modules', packageName);
const packageJson = readJson<FoundryPackageJson>(path.join(packageRoot, 'package.json'));
if (packageJson.version !== version) {
throw new Error(`Expected ${packageName}@${version}, found ${packageJson.version ?? 'an unknown version'}`);
}
if (packageJson.scripts?.preinstall !== 'node script/preinstall.cjs' || packageJson.scripts.install !== 'node script/install-standard.cjs') {
throw new Error(`Unexpected ${packageName}@${version} lifecycle scripts`);
}

validateInstallerUtils(path.join(packageRoot, 'script', 'install-utils.cjs'));
runLifecycleScript(packageRoot, 'script/preinstall.cjs');

const target = `${process.platform}-${process.arch}`;
if (!supportsCoreLibraryTarget(target)) {
console.warn(`[foundry-local] Unsupported platform: ${target}. Skipping.`);
return;
}

const dependencies = readJson<IFoundryDependencyVersions>(path.join(packageRoot, 'deps_versions.json'));
const artifacts = getStandardArtifacts(target, dependencies);
const binDir = path.join(packageRoot, 'foundry-local-core', target);
await fetchCoreLibraries(target, artifacts, binDir, { feeds: [VSCODE_NUGET_FEED], skipIfPresent: true });

const missingFiles = requiredCoreLibraryNames(target).filter(file => !fs.existsSync(path.join(binDir, file)));
if (missingFiles.length > 0) {
throw new Error(`[foundry-local] Missing required native libraries for ${target}: ${missingFiles.join(', ')}`);
}

const coreVersion = dependencies['foundry-local-core'].nuget;
const platformPackageJson = {
name: `@foundry-local-core/${target}`,
version: coreVersion,
description: `Native binaries for Foundry Local SDK (${target})`,
private: true,
};
fs.writeFileSync(path.join(binDir, 'package.json'), JSON.stringify(platformPackageJson, undefined, 2));
console.log('[foundry-local] Installation complete.');
}

if (import.meta.filename === process.argv[1]) {
await installFoundryLocal();
}
5 changes: 4 additions & 1 deletion build/azure-pipelines/common/sanity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ jobs:
displayName: Create Crash Dumps Directory

- ${{ if and(eq(parameters.os, 'windows'), eq(parameters.arch, 'arm64')) }}:
- task: NuGetAuthenticate@1
displayName: Setup NuGet Authentication

- script: |
@echo off
setlocal enabledelayedexpansion
Expand All @@ -117,7 +120,7 @@ jobs:
if exist "!SDK_ROOT!" rmdir /s /q "!SDK_ROOT!"

set "SDK_PACKAGE=$(Agent.TempDirectory)\windows-sdk-build-tools.nupkg"
curl.exe -fsSL --retry 5 --retry-delay 2 --retry-all-errors "https://api.nuget.org/v3-flatcontainer/microsoft.windows.sdk.buildtools/!PACKAGE_VERSION!/microsoft.windows.sdk.buildtools.!PACKAGE_VERSION!.nupkg" -o "!SDK_PACKAGE!"
curl.exe -fsSL --retry 5 --retry-delay 2 --retry-all-errors -u "vscode:%VSS_NUGET_ACCESSTOKEN%" "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/nuget/v3/flat2/microsoft.windows.sdk.buildtools/!PACKAGE_VERSION!/microsoft.windows.sdk.buildtools.!PACKAGE_VERSION!.nupkg" -o "!SDK_PACKAGE!"

set "ACTUAL_HASH="
for /f "skip=1" %%A in ('certutil -hashfile "!SDK_PACKAGE!" SHA256') do if not defined ACTUAL_HASH set "ACTUAL_HASH=%%A"
Expand Down
Loading
Loading