Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e534b2c
Share secrets between Code and Agents app via macOS Keychain
alexdima Apr 10, 2026
65e9db5
Merge remote-tracking branch 'origin/main' into alexdima/shared-keychain
alexdima Apr 10, 2026
a76b952
Address review feedback
alexdima Apr 10, 2026
d043322
Add one-time migration of legacy secrets to shared keychain
alexdima Apr 10, 2026
3beb9b9
update the current implementation
alexdima Apr 10, 2026
d41f5af
restrict shared keychain to CROSS_APP_SHARED_SECRET_KEYS
alexdima Apr 10, 2026
11b7c37
kick off shared keychain migration eagerly in constructor
alexdima Apr 10, 2026
569fdfb
update @vscode/macos-keychain to 0.0.1
alexdima Apr 10, 2026
a1c8f52
Use provisioning profile for keychain access groups when available
alexdima Apr 10, 2026
ec06545
Add entitlements diagnostic dump after signing
alexdima Apr 11, 2026
2b5329c
Exclude provisioning profile from unicode hygiene check
alexdima Apr 11, 2026
70abdc1
Merge remote-tracking branch 'origin/main' into alexdima/shared-keychain
alexdima Apr 22, 2026
08b8503
update package-lock.json
alexdima Apr 22, 2026
4772169
Adopt multiple provision profiles
alexdima Apr 22, 2026
720d141
fix: expand teamidentifier in the entitlement
deepak1556 Apr 22, 2026
6635fb6
Re-sign without provisioning profile for tests
alexdima Apr 23, 2026
1cb20cf
Skip plist modifications when re-signing for tests
alexdima Apr 23, 2026
24838ca
Merge remote-tracking branch 'origin/main' into alexdima/shared-keychain
alexdima Apr 23, 2026
dfb67da
Move shared keychain migration from renderer to main process
alexdima Apr 24, 2026
0622a64
Merge remote-tracking branch 'origin/main' into alexdima/shared-keychain
alexdima Apr 24, 2026
f7c2ced
Add isMacintosh guards before using the shared keychain service
alexdima Apr 24, 2026
efe3661
Remove spec
alexdima Apr 26, 2026
8a565ba
Tweak comments
alexdima Apr 26, 2026
40cc75e
nes: support neighbor files to be included in NES prompt
ulugbekna Apr 26, 2026
d603ad4
Add 'last two messages' cache breakpoint strategy for Messages API (#…
bhavyaus Apr 26, 2026
bccdfa8
copilot agent type cleanup (#312611)
roblourens Apr 26, 2026
b4347c5
Share secrets between Code and Agents app via macOS Keychain (#308990)
alexdima Apr 26, 2026
fe0c770
promptValidator: log error (#311899)
aeschli Apr 26, 2026
1d8e3cb
round trip permission levels to extension (#312479)
justschen Apr 26, 2026
15d2d8c
Fix agent host agents in vscode (#312628)
roblourens Apr 26, 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
8 changes: 8 additions & 0 deletions build/.moduleignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ vsda/**
@vscode/policy-watcher/index.d.ts
!@vscode/policy-watcher/build/Release/vscode-policy-watcher.node

@vscode/macos-keychain/build/**
@vscode/macos-keychain/src/**
@vscode/macos-keychain/test/**
@vscode/macos-keychain/binding.gyp
@vscode/macos-keychain/README.md
@vscode/macos-keychain/index.d.ts
!@vscode/macos-keychain/build/Release/keychainNative.node

@vscode/windows-ca-certs/**/*
!@vscode/windows-ca-certs/package.json
!@vscode/windows-ca-certs/**/*.node
Expand Down
4 changes: 4 additions & 0 deletions build/azure-pipelines/darwin/app-entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(TeamIdentifierPrefix)com.microsoft.vscode.shared-secrets</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,16 @@ steps:
BUILD_SOURCESDIRECTORY: $(Build.SourcesDirectory)
displayName: ✍️ Codesign & Notarize

# Re-sign the app without the provisioning profile for tests.
# This strips the keychain-access-groups entitlement which requires a
# provisioning profile and is not needed for running tests. The codesign
# step reads from the archives packaged above which have the full entitlements.
- script: |
set -e
export CODESIGN_IDENTITY=$(security find-identity -v -p codesigning $(agent.tempdirectory)/buildagent.keychain | grep -oEi "([0-9A-F]{40})" | head -n 1)
DEBUG=electron-osx-sign* node build/darwin/sign.ts --skip-provisioning-profile $(agent.builddirectory)
displayName: Set Hardened Entitlements (for tests)
- ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}:
- template: product-build-darwin-test.yml@self
parameters:
Expand Down
102 changes: 95 additions & 7 deletions build/darwin/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,66 @@ function getElectronVersion(): string {
return target;
}

function getEntitlementsForFile(filePath: string): string {
const mainProvisioningProfilePath = path.join(baseDir, 'darwin', 'main.provisionprofile');
const agentsProvisioningProfilePath = path.join(baseDir, 'darwin', 'agents.provisionprofile');

function hasProvisioningProfile(): boolean {
return fs.existsSync(mainProvisioningProfilePath);
}

function getEntitlementsForFile(filePath: string, tempDir: string, useProvisioningProfile: boolean, teamId?: string): string {
if (filePath.includes(' Helper (GPU).app')) {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist');
} else if (filePath.includes(' Helper (Renderer).app')) {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist');
} else if (filePath.includes(' Helper (Plugin).app')) {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist');
}
return path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
const entitlementsPath = path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
if (!useProvisioningProfile) {
// Without a provisioning profile, keychain-access-groups entitlement
// will cause signing failures. Strip it from the entitlements plist.
return getStrippedEntitlements(entitlementsPath, tempDir);
}
if (teamId) {
return getExpandedEntitlements(entitlementsPath, tempDir, teamId);
}
return entitlementsPath;
}

let _strippedEntitlementsPath: string | undefined;

/**
* Returns a path to a copy of the entitlements plist with the
* keychain-access-groups key removed.
*/
function getStrippedEntitlements(entitlementsPath: string, tempDir: string): string {
if (!_strippedEntitlementsPath) {
const content = fs.readFileSync(entitlementsPath, 'utf8');
const stripped = content.replace(
/\s*<key>keychain-access-groups<\/key>\s*<array>[\s\S]*?<\/array>/,
''
);
_strippedEntitlementsPath = path.join(tempDir, 'app-entitlements-stripped.plist');
fs.writeFileSync(_strippedEntitlementsPath, stripped);
}
return _strippedEntitlementsPath;
}

let expandedEntitlementsPath: string | undefined;

/**
* Returns a path to a copy of the entitlements plist with
* $(TeamIdentifierPrefix) expanded to the actual team identifier.
*/
function getExpandedEntitlements(entitlementsPath: string, tempDir: string, teamId: string): string {
if (!expandedEntitlementsPath) {
const content = fs.readFileSync(entitlementsPath, 'utf8');
const expanded = content.replace(/\$\(TeamIdentifierPrefix\)/g, teamId + '.');
expandedEntitlementsPath = path.join(tempDir, 'app-entitlements.plist');
fs.writeFileSync(expandedEntitlementsPath, expanded);
}
return expandedEntitlementsPath;
}

async function retrySignOnKeychainError<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
Expand Down Expand Up @@ -58,7 +109,7 @@ async function retrySignOnKeychainError<T>(fn: () => Promise<T>, maxRetries: num
throw lastError;
}

async function main(buildDir?: string): Promise<void> {
async function main(buildDir?: string, skipProvisioningProfile?: boolean): Promise<void> {
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
const arch = process.env['VSCODE_ARCH'];
const identity = process.env['CODESIGN_IDENTITY'];
Expand All @@ -78,23 +129,51 @@ async function main(buildDir?: string): Promise<void> {
? path.resolve(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`, 'Contents', 'Info.plist')
: undefined;

const useProvisioningProfile = !skipProvisioningProfile && hasProvisioningProfile();
const resolvedProvisioningProfile = useProvisioningProfile ? mainProvisioningProfilePath : undefined;

let teamId: string | undefined;
if (resolvedProvisioningProfile) {
const profilePlist = await spawn('security', ['cms', '-D', '-i', resolvedProvisioningProfile]);
const teamIdMatch = /<key>TeamIdentifier<\/key>\s*<array>\s*<string>(.*?)<\/string>/s.exec(profilePlist);
if (teamIdMatch) {
teamId = teamIdMatch[1];
console.log(`Extracted TeamIdentifier from provisioning profile: ${teamId}`);
} else {
console.warn('Could not extract TeamIdentifier from provisioning profile; $(TeamIdentifierPrefix) will not be expanded');
}
}

// Embed the agents provisioning profile into the embedded app bundle
// before signing, since @electron/osx-sign only supports one top-level profile.
if (useProvisioningProfile && product.embedded && fs.existsSync(agentsProvisioningProfilePath)) {
const embeddedAppPath = path.join(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`);
if (fs.existsSync(embeddedAppPath)) {
const embeddedProfileDest = path.join(embeddedAppPath, 'Contents', 'embedded.provisionprofile');
fs.copyFileSync(agentsProvisioningProfilePath, embeddedProfileDest);
console.log(`Embedded agents provisioning profile into ${embeddedProfileDest}`);
}
}

const appOpts: SignOptions = {
app: path.join(appRoot, appName),
platform: 'darwin',
optionsForFile: (filePath) => ({
entitlements: getEntitlementsForFile(filePath),
entitlements: getEntitlementsForFile(filePath, tempDir, useProvisioningProfile, teamId),
hardenedRuntime: true,
}),
preAutoEntitlements: false,
preEmbedProvisioningProfile: false,
preEmbedProvisioningProfile: !!resolvedProvisioningProfile,
provisioningProfile: resolvedProvisioningProfile,
keychain: path.join(tempDir, 'buildagent.keychain'),
version: getElectronVersion(),
identity,
};

// Only overwrite plist entries for x64 and arm64 builds,
// universal will get its copy from the x64 build.
if (arch !== 'universal') {
// Skip when re-signing (skipProvisioningProfile) since entries already exist.
if (arch !== 'universal' && !skipProvisioningProfile) {
await spawn('plutil', [
'-insert',
'NSAppleEventsUsageDescription',
Expand Down Expand Up @@ -171,10 +250,19 @@ async function main(buildDir?: string): Promise<void> {
}

await retrySignOnKeychainError(() => sign(appOpts));

// Dump entitlements from the signed binary for diagnostic purposes
const mainBinary = path.join(appRoot, appName, 'Contents', 'MacOS', product.nameShort);
console.log(`Dumping entitlements from signed binary: ${mainBinary}`);
const entitlementsDump = await spawn('codesign', ['--display', '--entitlements', '-', '--xml', mainBinary]);
console.log(`Signed entitlements:\n${entitlementsDump}`);
}

if (import.meta.main) {
main(process.argv[2]).catch(async err => {
const args = process.argv.slice(2);
const skipProvisioningProfile = args.includes('--skip-provisioning-profile');
const buildDir = args.filter(a => !a.startsWith('--'))[0];
main(buildDir, skipProvisioningProfile).catch(async err => {
console.error(err);
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
if (tempDir) {
Expand Down
9 changes: 9 additions & 0 deletions extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3815,6 +3815,15 @@
"clear-both"
]
},
"github.copilot.chat.anthropic.cacheBreakpoints.lastTwoMessages": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.anthropic.cacheBreakpoints.lastTwoMessages%",
"tags": [
"experimental",
"onExp"
]
},
"github.copilot.chat.responsesApiReasoningSummary": {
"type": "string",
"default": "detailed",
Expand Down
1 change: 1 addition & 0 deletions extensions/copilot/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@
"copilot.toolSet.web.description": "Fetch information from the web",
"github.copilot.config.useMessagesApi": "Use the Messages API instead of the Chat Completions API when supported.",
"github.copilot.config.anthropic.contextEditing.mode": "Select the context editing mode for Anthropic models. Automatically manages conversation context as it grows, helping optimize costs and stay within context window limits.\n\n- `off`: Context editing is disabled.\n- `clear-thinking`: Clears thinking blocks while preserving tool uses.\n- `clear-tooluse`: Clears tool uses while preserving thinking blocks.\n- `clear-both`: Clears both thinking blocks and tool uses.\n\n**Note**: This is an experimental feature. Context editing may cause additional cache rewrites. Enable with caution.",
"github.copilot.config.anthropic.cacheBreakpoints.lastTwoMessages": "Use the 'last two messages' cache breakpoint strategy instead of heuristic-based placement for Anthropic Messages API.",
"github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: URL API path resolution for custom OpenAI-compatible and Azure models is independent of this setting and fully determined by `url` property of `#github.copilot.chat.customOAIModels#` or `#github.copilot.chat.azureModels#` respectively.",
"github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
"github.copilot.config.responsesApiContextManagement.enabled": "Enables context management for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco

// #region Chat Participant Handler

provideHandleOptionsChange(resource: vscode.Uri, updates: ReadonlyArray<vscode.ChatSessionOptionUpdate>, _token: vscode.CancellationToken): void {
const sessionId = ClaudeSessionUri.getSessionId(resource);
for (const update of updates) {
const value = update.value;
if (update.optionId === PERMISSION_MODE_OPTION_ID && value && isPermissionMode(value)) {
this.sessionStateService.setPermissionModeForSession(sessionId, value);
}
}
}

createHandler(): ChatExtendedRequestHandler {
return async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> => {
const { chatSessionContext } = context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { convertReferenceToVariable } from '../copilotcli/vscode-node/copilotCLI
import { clearChangesCacheForAffectedSessions } from './chatSessionRepositoryTracker';

const REPOSITORY_OPTION_ID = 'repository';
const PERMISSION_LEVEL_OPTION_ID = 'permissionLevel';

const _sessionWorktreeIsolationCache = new Map<string, boolean>();
const BRANCH_OPTION_ID = 'branch';
Expand Down Expand Up @@ -578,6 +579,7 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
private _currentSessionId: string | undefined;
private _selectedRepoForBranches: { repoUri: URI; headBranchName: string | undefined } | undefined;
private _displayedOptionIds = new Set<string>();
private readonly _activeSessionsById = new Map<string, ICopilotCLISession>();
/**
* ID of the last used folder in an untitled workspace (for defaulting selection).
*/
Expand Down Expand Up @@ -1076,7 +1078,10 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
const wasBranchOptionShow = !!this._selectedRepoForBranches;
let triggerProviderOptionsChange = false;
for (const update of updates) {
if (update.optionId === REPOSITORY_OPTION_ID && typeof update.value === 'string' && this.sessionItemProvider.isNewSession(sessionId)) {
if (update.optionId === PERMISSION_LEVEL_OPTION_ID) {
const level = typeof update.value === 'string' ? update.value : undefined;
this._getActiveSessionForResourceId(sessionId)?.setPermissionLevel(level);
} else if (update.optionId === REPOSITORY_OPTION_ID && typeof update.value === 'string' && this.sessionItemProvider.isNewSession(sessionId)) {
const folder = vscode.Uri.file(update.value);
if (isEqual(folder, this._selectedRepoForBranches?.repoUri)) {
continue;
Expand Down Expand Up @@ -1184,6 +1189,29 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
}
}

private _getActiveSessionForResourceId(sessionId: string): ICopilotCLISession | undefined {
return this._activeSessionsById.get(this.sessionItemProvider.untitledSessionIdMapping.get(sessionId) ?? sessionId)
?? this._activeSessionsById.get(sessionId);
}

trackActiveSession(resourceSessionId: string, session: ICopilotCLISession): void {
this._activeSessionsById.set(resourceSessionId, session);
this._activeSessionsById.set(session.sessionId, session);
}

untrackActiveSession(resourceSessionId: string | undefined, session: ICopilotCLISession | undefined, hasPendingRequests: boolean): void {
if (!session || hasPendingRequests) {
return;
}

if (resourceSessionId && this._activeSessionsById.get(resourceSessionId) === session) {
this._activeSessionsById.delete(resourceSessionId);
}
if (this._activeSessionsById.get(session.sessionId) === session) {
this._activeSessionsById.delete(session.sessionId);
}
}

}

function toRepositoryOptionItem(repository: RepoContext | Uri, isDefault: boolean = false): ChatSessionProviderOptionItem {
Expand Down Expand Up @@ -1348,7 +1376,9 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
const disposables = new DisposableStore();
let sessionId: string | undefined = undefined;
let sessionParentId: string | undefined = undefined;
let sessionPermissionLevel: string | undefined = undefined;
let sdkSessionId: string | undefined = undefined;
let activeSession: ICopilotCLISession | undefined;
try {

const initialOptions = chatSessionContext?.initialSessionOptions;
Expand All @@ -1365,6 +1395,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
_sessionBranch.set(sessionId, value);
} else if (opt.optionId === ISOLATION_OPTION_ID && value) {
_sessionIsolation.set(sessionId, value as IsolationMode);
} else if (opt.optionId === PERMISSION_LEVEL_OPTION_ID && value) {
sessionPermissionLevel = value;
} else if (opt.optionId === PARENT_SESSION_OPTION_ID && value) {
sessionParentId = value;
}
Expand Down Expand Up @@ -1453,7 +1485,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
};
const newBranch = (isUntitled && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : undefined;

const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent, newBranch, sessionParentId }, disposables, token);
const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent, newBranch, sessionParentId, permissionLevel: sessionPermissionLevel }, disposables, token);
const session = sessionResult.session;
if (session) {
disposables.add(session);
Expand All @@ -1472,6 +1504,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
}

sdkSessionId = session.object.sessionId;
activeSession = session.object;
this.contentProvider.trackActiveSession(sessionId, activeSession);
const modeInstructions = this.createModeInstructions(request);
this.chatSessionMetadataStore.updateRequestDetails(sessionId, [{ vscodeRequestId: request.id, agentId: agent?.name ?? '', modeInstructions }]).catch(ex => this.logService.error(ex, 'Failed to update request details'));

Expand Down Expand Up @@ -1565,6 +1599,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
}
}
}
this.contentProvider.untrackActiveSession(sessionId, activeSession, sdkSessionId ? this.pendingRequestBySession.has(sdkSessionId) : false);
if (chatSessionContext?.chatSessionItem.resource) {
this.sessionItemProvider.notifySessionsChange();
}
Expand Down Expand Up @@ -1831,7 +1866,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
}
}

private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; newBranch?: Promise<string | undefined>; sessionParentId?: string }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; trusted: boolean }> {
private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; newBranch?: Promise<string | undefined>; sessionParentId?: string; permissionLevel?: string }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; trusted: boolean }> {
const { resource } = chatSessionContext.chatSessionItem;
const existingSessionId = this.sessionItemProvider.untitledSessionIdMapping.get(SessionIdForCLI.parse(resource));
const id = existingSessionId ?? SessionIdForCLI.parse(resource);
Expand Down Expand Up @@ -1872,7 +1907,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
void this.workspaceFolderService.trackSessionWorkspaceFolder(session.object.sessionId, sessionWorkingDirectory.fsPath, session.object.workspace.repositoryProperties);
}
disposables.add(session.object.attachStream(stream));
const permissionLevel = request.permissionLevel;
const permissionLevel = request.permissionLevel ?? options.permissionLevel;
session.object.setPermissionLevel(permissionLevel);

return { session, trusted };
Expand Down
Loading
Loading