From 836b756ee2d8a553d9b26cf095e12979141e2367 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 24 Aug 2026 14:39:27 -0700 Subject: [PATCH 1/4] fix: surface backend interactive mode in getTransformInfo getTransformInfo already resolves the interactive mode from the job objective (cachedInteractiveMode) but never returned it. Add the InteractiveMode field to AtxGetTransformInfoResponse and populate it at the getTransformInfo wrapper so the IDE can restore the correct mode after a restart instead of trusting its local settings store. Paired with the IDE change in aws-toolkit-visual-studio-staging. --- .../src/language-server/netTransform/atxModels.ts | 4 ++++ .../src/language-server/netTransform/atxTransformHandler.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts index 21622fea61..f010011550 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -146,6 +146,10 @@ export interface AtxGetTransformInfoResponse { MissingPackageJsonPath?: string | null DiffApplyFailed?: boolean DiffApplyFailedStepIds?: string[] + // Interactive mode as resolved from the backend job objective (interactive_mode). + // Surfaced so the IDE can restore the correct mode instead of relying on its local + // settings store, which may be missing/stale (e.g. cold restart on another machine). + InteractiveMode?: InteractiveMode } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index f0e9abfe2e..04de8649da 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -1770,6 +1770,12 @@ export class ATXTransformHandler { result.DiffApplyFailed = true result.DiffApplyFailedStepIds = diffContext.failedStepIds } + // Surface the backend-resolved interactive mode (from job.objective) on every + // response so the IDE can restore it. Single injection point covers all internal + // return paths. cachedInteractiveMode is populated in _getTransformInfoInternal. + if (result && this.cachedInteractiveMode) { + result.InteractiveMode = this.cachedInteractiveMode + } return result } finally { this._currentDiffContext = null From 529aed43259503cf71b475fb3496de7bfab25f17 Mon Sep 17 00:00:00 2001 From: chungjac Date: Mon, 24 Aug 2026 23:57:13 +0000 Subject: [PATCH 2/4] fix(amazonq): cover merged env and headers in MCP consent fingerprint (#2851) (#2853) --- .../tools/mcp/mcpConsentStore.test.ts | 92 ++++++++++++++++++- .../agenticChat/tools/mcp/mcpConsentStore.ts | 42 ++++++++- .../agenticChat/tools/mcp/mcpManager.ts | 27 +++++- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts index 79696b5f9d..b638968139 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts @@ -8,6 +8,8 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import { + effectiveEnv, + effectiveHeaders, fingerprintServerConfig, fingerprintWorkspace, hasApproval, @@ -74,6 +76,71 @@ describe('mcpConsentStore', () => { const b: MCPServerConfig = { url: 'https://b.example' } expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) }) + + // __additionalEnv__ / __additionalHeaders__ are merged into the spawn env/headers, + // so they must be covered by the fingerprint alongside the raw fields — otherwise a + // post-approval config edit to those fields would not re-prompt. + it('differs when __additionalEnv__ is added', () => { + const a: MCPServerConfig = { command: 'npx', args: ['-y', 's'], env: { LOG_LEVEL: 'info' } } + const b: MCPServerConfig = { + ...a, + __additionalEnv__: { EXTRA: '1' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalEnv__ overrides an existing env value', () => { + const a: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const b: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' }, __additionalEnv__: { FOO: '2' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when headers change', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer attacker' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalHeaders__ overrides an existing header', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { + url: 'https://a.example', + headers: { Authorization: 'Bearer good' }, + __additionalHeaders__: { Authorization: 'Bearer attacker' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + // Consent is about what will execute, not how the config is spelled: two configs + // that spawn the process with the identical effective env share a fingerprint. + it('hashes the merged spawn env, so the same effective env matches either field', () => { + const viaEnv: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const viaAdditional: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { FOO: '1' } } + expect(fingerprintServerConfig(viaEnv)).to.equal(fingerprintServerConfig(viaAdditional)) + }) + + it('is stable regardless of __additionalEnv__ key order', () => { + const a: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { A: '1', B: '2' } } + const b: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { B: '2', A: '1' } } + expect(fingerprintServerConfig(a)).to.equal(fingerprintServerConfig(b)) + }) + }) + + describe('effectiveEnv / effectiveHeaders', () => { + it('merges __additionalEnv__ over env, matching the spawn-time merge', () => { + const cfg: MCPServerConfig = { env: { A: '1', B: '2' }, __additionalEnv__: { B: 'override', C: '3' } } + expect(effectiveEnv(cfg)).to.deep.equal({ A: '1', B: 'override', C: '3' }) + }) + + it('merges __additionalHeaders__ over headers', () => { + const cfg: MCPServerConfig = { headers: { X: '1' }, __additionalHeaders__: { X: '2', Y: '3' } } + expect(effectiveHeaders(cfg)).to.deep.equal({ X: '2', Y: '3' }) + }) + + it('returns an empty object when nothing is set', () => { + expect(effectiveEnv({})).to.deep.equal({}) + expect(effectiveHeaders({})).to.deep.equal({}) + }) }) describe('fingerprintWorkspace', () => { @@ -183,11 +250,34 @@ describe('mcpConsentStore', () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) fs.writeFileSync(path.join(storeDir, 'mcp-approvals.json'), JSON.stringify({ version: 999, approvals: [] })) - // record should still work (overwrites with v1) + // record should still work (overwrites with the current version) await recordApproval(workspace, logger, 'poc', cfg, configPath) expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true }) + // STORE_VERSION 1 -> 2: v1 fingerprints were computed over a narrower field set and + // cannot be trusted to cover the merged env/headers, so they are discarded and the + // user is re-prompted once per workspace-scoped server after upgrade. + it('discards legacy v1 approvals so the user is re-prompted once after upgrade', async () => { + const storeDir = path.join(tmpHome, '.aws', 'amazonq') + fs.mkdirSync(storeDir, { recursive: true }) + fs.writeFileSync( + path.join(storeDir, 'mcp-approvals.json'), + JSON.stringify({ + version: 1, + approvals: [ + { + serverName: 'poc', + fingerprint: fingerprintServerConfig(cfg), + workspaceHash: fingerprintWorkspace(configPath), + approvedAt: new Date().toISOString(), + }, + ], + }) + ) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + it('treats a malformed store as empty', async () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts index b9230a6655..45a4f252fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts @@ -9,7 +9,11 @@ import type { Workspace, Logging } from '@aws/language-server-runtimes/server-in import type { MCPServerConfig } from './mcpTypes' const APPROVALS_FILE = 'mcp-approvals.json' -const STORE_VERSION = 1 +// v2: the fingerprint now covers the fully-merged spawn environment and headers +// (see fingerprintServerConfig). Bumping the version discards v1 approvals, which +// were computed over a narrower field set, so every workspace-scoped server is +// re-consented once after upgrade. +const STORE_VERSION = 2 interface Approval { serverName: string @@ -23,17 +27,49 @@ interface ApprovalStore { approvals: Approval[] } +function sortedRecord(rec?: Record): Record { + return rec ? Object.fromEntries(Object.entries(rec).sort(([a], [b]) => a.localeCompare(b))) : {} +} + +/** + * The environment the stdio transport will actually spawn the server with, as far + * as the config controls it. Mirrors the merge in McpManager (`cfg.env` overlaid by + * `cfg.__additionalEnv__`). + * + * `__additionalEnv__` carries workspace/agent-level `env` for registry servers and is + * NOT folded into `cfg.env`, so it must be merged here or it escapes the fingerprint. + */ +export function effectiveEnv(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.env ?? {}), ...(cfg.__additionalEnv__ ?? {}) }) +} + +/** + * The headers the HTTP/SSE transport will actually send, as far as the config + * controls it. Mirrors the merge in McpManager (`cfg.headers` overlaid by + * `cfg.__additionalHeaders__`). + */ +export function effectiveHeaders(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.headers ?? {}), ...(cfg.__additionalHeaders__ ?? {}) }) +} + /** * SHA-256 of a canonical JSON form of the server's execution-relevant fields. - * Any change to command/args/env/url yields a new fingerprint, invalidating + * Any change to command/args/env/url/headers yields a new fingerprint, invalidating * prior approvals — so mutation of the config re-prompts. + * + * `env` and `headers` are hashed in their *merged* form (see effectiveEnv / + * effectiveHeaders) so every field that reaches the spawned process is covered by + * consent. Two configs that spawn an identical process share a fingerprint regardless + * of which field supplied a value: consent is about what will execute, not how the + * config is spelled. */ export function fingerprintServerConfig(cfg: MCPServerConfig): string { const canonical = { command: cfg.command ?? null, args: cfg.args ?? [], - env: cfg.env ? Object.fromEntries(Object.entries(cfg.env).sort(([a], [b]) => a.localeCompare(b))) : {}, + env: effectiveEnv(cfg), url: cfg.url ?? null, + headers: effectiveHeaders(cfg), } return 'sha256:' + createHash('sha256').update(JSON.stringify(canonical)).digest('hex') } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 61e7ea75fe..bde82719d7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -42,7 +42,14 @@ import { Mutex } from 'async-mutex' import path = require('path') import { URI } from 'vscode-uri' import { MessageType } from '@aws/language-server-runtimes/protocol' -import { hasApproval, recordApproval, removeApproval, fingerprintServerConfig } from './mcpConsentStore' +import { + hasApproval, + recordApproval, + removeApproval, + fingerprintServerConfig, + effectiveEnv, + effectiveHeaders, +} from './mcpConsentStore' import { sanitizeInput } from '../../../../shared/utils' import { ProfileStatusMonitor } from './profileStatusMonitor' import { OAuthClient } from './mcpOauthClient' @@ -440,6 +447,17 @@ export class McpManager { ) if (!approved) { const cmdLine = [cfg.command ?? cfg.url ?? '(none)', ...(cfg.args ?? [])].join(' ').slice(0, 200) + // Surface the environment variables and headers the server will actually be + // launched with. Names only, never values: a config may legitimately hold + // tokens, and this string is shown in a dialog and written to logs. Values are + // covered by the fingerprint, so any value change re-prompts. + const envKeys = Object.keys(effectiveEnv(cfg)) + const headerNames = Object.keys(effectiveHeaders(cfg)) + const envLine = + envKeys.length > 0 + ? `Environment variables: ${envKeys.join(', ').slice(0, 200)}\n` + : `Environment variables: (none)\n` + const headerLine = headerNames.length > 0 ? `Headers: ${headerNames.join(', ').slice(0, 200)}\n` : '' const allowBtn = { title: 'Allow for this server' } const denyBtn = { title: 'Deny' } let choice: { title: string } | null | undefined @@ -451,8 +469,13 @@ export class McpManager { `A workspace configuration file wants to start an MCP server.\n` + `Server: ${serverName}\n` + `Command: ${cmdLine}\n` + + envLine + + headerLine + `Source: ${configPath}\n\n` + - `Running this server executes the above command on your machine. ` + + `Running this server executes the above command on your machine, ` + + `with the environment variables listed above. ` + + `Review them in the configuration file if you are unsure — variables such as ` + + `NODE_OPTIONS can cause additional code to run. ` + `Only allow if you trust the authors of this workspace.\n\n` + `Your choice will be remembered for this workspace. ` + `If you allow, you won't be asked again unless the server configuration changes.`, From d493511cc333dd6a1b7d40b056ea807e41cd8274 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:55:49 -0700 Subject: [PATCH 3/4] chore(release): release packages from branch main (#2850) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- package-lock.json | 2 +- server/aws-lsp-codewhisperer/CHANGELOG.md | 9 +++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bb45dcb8fd..c2a9a56c93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.56", "core/aws-lsp-core": "0.0.22", "server/aws-lsp-antlr4": "0.1.26", - "server/aws-lsp-codewhisperer": "0.0.126", + "server/aws-lsp-codewhisperer": "0.0.127", "server/aws-lsp-json": "0.1.27", "server/aws-lsp-partiql": "0.0.24", "server/aws-lsp-yaml": "0.1.27" diff --git a/package-lock.json b/package-lock.json index 4aed07ac92..81579468b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30351,7 +30351,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.126", + "version": "0.0.127", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index b7c1051c23..19f7b5fd2e 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.0.127](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.126...lsp-codewhisperer/v0.0.127) (2026-08-24) + + +### Bug Fixes + +* **amazonq:** cover merged env and headers in MCP consent fingerprint ([#2851](https://github.com/Amazon-Q-Developer/language-servers/issues/2851)) ([#2853](https://github.com/Amazon-Q-Developer/language-servers/issues/2853)) ([529aed4](https://github.com/Amazon-Q-Developer/language-servers/commit/529aed43259503cf71b475fb3496de7bfab25f17)) +* beam - flat-named transformed zips + lightweight discovery + IsLbvPending ([#2849](https://github.com/Amazon-Q-Developer/language-servers/issues/2849)) ([863c5bf](https://github.com/Amazon-Q-Developer/language-servers/commit/863c5bf00a6c30ec7658056a155f1ca1005127e6)) +* surface backend interactive mode in getTransformInfo ([836b756](https://github.com/Amazon-Q-Developer/language-servers/commit/836b756ee2d8a553d9b26cf095e12979141e2367)) + ## [0.0.126](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.125...lsp-codewhisperer/v0.0.126) (2026-08-20) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 4c2a195da7..af7b4aaa17 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.126", + "version": "0.0.127", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 2b5b9e7d3511284eb4a4775c90a2e2f229a531de Mon Sep 17 00:00:00 2001 From: XiaowenMaoA <107279155+XiaowenMaoA@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:44:37 -0700 Subject: [PATCH 4/4] chore: bump agentic version: 1.78.0 (#2856) Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 9fb2d82896..e8fa26028c 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.77.0" + "agenticChat": "1.78.0" }