From bd3951dbe6fd7d26d931a5d82b49af293318ff08 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Tue, 24 Feb 2026 17:38:34 -0500 Subject: [PATCH] feat: set gateway env vars in agentcore dev for local testing Read deployed-state.json for gateway URLs and mcp.json for auth types, then set AGENTCORE_GATEWAY_{NAME}_URL and AGENTCORE_GATEWAY_{NAME}_AUTH_TYPE env vars when running agentcore dev locally. - New gateway-env.ts helper iterates all deployment targets - Integrated in both CLI dev command and TUI dev hook - .env.local values take precedence over gateway env vars - Graceful fallback when no deployed state exists - Fixed parseGatewayOutputs to parse Id, Arn, and Url outputs separately - Added gatewayUrl field to deployed-state schema (optional, backward compat) --- .../cloudformation/__tests__/outputs.test.ts | 25 +++++++++++----- src/cli/cloudformation/outputs.ts | 22 +++++++++----- src/cli/commands/dev/command.tsx | 6 +++- src/cli/operations/dev/gateway-env.ts | 30 +++++++++++++++++++ src/cli/tui/hooks/useDevServer.ts | 6 +++- src/schema/schemas/deployed-state.ts | 1 + 6 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 src/cli/operations/dev/gateway-env.ts diff --git a/src/cli/cloudformation/__tests__/outputs.test.ts b/src/cli/cloudformation/__tests__/outputs.test.ts index 6d1091f3..39745b4a 100644 --- a/src/cli/cloudformation/__tests__/outputs.test.ts +++ b/src/cli/cloudformation/__tests__/outputs.test.ts @@ -112,9 +112,13 @@ describe('buildDeployedState', () => { }); describe('parseGatewayOutputs', () => { - it('extracts gateway URL from outputs matching pattern', () => { + it('extracts gateway outputs matching pattern', () => { const outputs = { + GatewayMyGatewayIdOutput3E11FAB4: 'gw-123', + GatewayMyGatewayArnOutput3E11FAB4: 'arn:aws:bedrock:us-east-1:123:gateway/gw-123', GatewayMyGatewayUrlOutput3E11FAB4: 'https://api.gateway.url', + GatewayAnotherGatewayIdOutputABC123: 'gw-456', + GatewayAnotherGatewayArnOutputABC123: 'arn:aws:bedrock:us-east-1:123:gateway/gw-456', GatewayAnotherGatewayUrlOutputABC123: 'https://another.gateway.url', UnrelatedOutput: 'some-value', }; @@ -128,12 +132,14 @@ describe('parseGatewayOutputs', () => { expect(result).toEqual({ 'my-gateway': { - gatewayId: 'my-gateway', - gatewayArn: 'https://api.gateway.url', + gatewayId: 'gw-123', + gatewayArn: 'arn:aws:bedrock:us-east-1:123:gateway/gw-123', + gatewayUrl: 'https://api.gateway.url', }, 'another-gateway': { - gatewayId: 'another-gateway', - gatewayArn: 'https://another.gateway.url', + gatewayId: 'gw-456', + gatewayArn: 'arn:aws:bedrock:us-east-1:123:gateway/gw-456', + gatewayUrl: 'https://another.gateway.url', }, }); }); @@ -155,8 +161,11 @@ describe('parseGatewayOutputs', () => { it('maps multiple gateways correctly', () => { const outputs = { + GatewayFirstGatewayArnOutput123: 'arn:first', GatewayFirstGatewayUrlOutput123: 'https://first.url', + GatewaySecondGatewayArnOutput456: 'arn:second', GatewaySecondGatewayUrlOutput456: 'https://second.url', + GatewayThirdGatewayArnOutput789: 'arn:third', GatewayThirdGatewayUrlOutput789: 'https://third.url', }; @@ -169,8 +178,8 @@ describe('parseGatewayOutputs', () => { const result = parseGatewayOutputs(outputs, gatewaySpecs); expect(Object.keys(result)).toHaveLength(3); - expect(result['first-gateway']?.gatewayArn).toBe('https://first.url'); - expect(result['second-gateway']?.gatewayArn).toBe('https://second.url'); - expect(result['third-gateway']?.gatewayArn).toBe('https://third.url'); + expect(result['first-gateway']?.gatewayUrl).toBe('https://first.url'); + expect(result['second-gateway']?.gatewayUrl).toBe('https://second.url'); + expect(result['third-gateway']?.gatewayUrl).toBe('https://third.url'); }); }); diff --git a/src/cli/cloudformation/outputs.ts b/src/cli/cloudformation/outputs.ts index 9cf5ae77..8f4ba433 100644 --- a/src/cli/cloudformation/outputs.ts +++ b/src/cli/cloudformation/outputs.ts @@ -38,8 +38,8 @@ export async function getStackOutputs(region: string, stackName: string): Promis export function parseGatewayOutputs( outputs: StackOutputs, gatewaySpecs: Record -): Record { - const gateways: Record = {}; +): Record { + const gateways: Record = {}; // Map PascalCase gateway names to original names for lookup const gatewayNames = Object.keys(gatewaySpecs); @@ -53,15 +53,21 @@ export function parseGatewayOutputs( if (!match) continue; const logicalGateway = match[1]; - if (!logicalGateway) continue; + const outputType = match[2]; + if (!logicalGateway || !outputType) continue; // Look up original gateway name from PascalCase version const gatewayName = gatewayIdMap.get(logicalGateway) ?? logicalGateway; - gateways[gatewayName] = { - gatewayId: gatewayName, - gatewayArn: value, - }; + gateways[gatewayName] ??= { gatewayId: gatewayName, gatewayArn: '' }; + + if (outputType === 'Id') { + gateways[gatewayName].gatewayId = value; + } else if (outputType === 'Arn') { + gateways[gatewayName].gatewayArn = value; + } else if (outputType === 'Url') { + gateways[gatewayName].gatewayUrl = value; + } } return gateways; @@ -173,7 +179,7 @@ export function buildDeployedState( targetName: string, stackName: string, agents: Record, - gateways: Record, + gateways: Record, existingState?: DeployedState, identityKmsKeyArn?: string, credentials?: Record diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index 427c04a6..374f446e 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -11,6 +11,7 @@ import { invokeAgentStreaming, loadProjectConfig, } from '../../operations/dev'; +import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js'; import { FatalError } from '../../tui/components'; import { LayoutProvider } from '../../tui/context'; import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; @@ -123,6 +124,9 @@ export const registerDev = (program: Command) => { const agentName = opts.agent ?? project.agents[0]?.name; const configRoot = findConfigRoot(workingDir); const envVars = configRoot ? await readEnvFile(configRoot) : {}; + const gatewayEnvVars = await getGatewayEnvVars(); + // Gateway env vars go first, .env.local overrides take precedence + const mergedEnvVars = { ...gatewayEnvVars, ...envVars }; const config = getDevConfig(workingDir, project, configRoot ?? undefined, agentName); if (!config) { @@ -164,7 +168,7 @@ export const registerDev = (program: Command) => { }, }; - const server = createDevServer(config, { port: actualPort, envVars, callbacks: devCallbacks }); + const server = createDevServer(config, { port: actualPort, envVars: mergedEnvVars, callbacks: devCallbacks }); await server.start(); // Handle Ctrl+C — use server.kill() for proper container cleanup diff --git a/src/cli/operations/dev/gateway-env.ts b/src/cli/operations/dev/gateway-env.ts new file mode 100644 index 00000000..78d43bcf --- /dev/null +++ b/src/cli/operations/dev/gateway-env.ts @@ -0,0 +1,30 @@ +import { ConfigIO } from '../../../lib/index.js'; + +export async function getGatewayEnvVars(): Promise> { + const configIO = new ConfigIO(); + const envVars: Record = {}; + + try { + const deployedState = await configIO.readDeployedState(); + const mcpSpec = configIO.configExists('mcp') ? await configIO.readMcpSpec() : undefined; + + // Iterate all targets (not just 'default') + for (const target of Object.values(deployedState?.targets ?? {})) { + const gateways = target?.resources?.mcp?.gateways ?? {}; + + for (const [name, gateway] of Object.entries(gateways)) { + if (!gateway.gatewayUrl) continue; + const sanitized = name.toUpperCase().replace(/-/g, '_'); + envVars[`AGENTCORE_GATEWAY_${sanitized}_URL`] = gateway.gatewayUrl; + + const gatewaySpec = mcpSpec?.agentCoreGateways?.find(g => g.name === name); + const authType = gatewaySpec?.authorizerType ?? 'NONE'; + envVars[`AGENTCORE_GATEWAY_${sanitized}_AUTH_TYPE`] = authType; + } + } + } catch { + // No deployed state or mcp.json — skip gateway env vars + } + + return envVars; +} diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 57b793c9..d7d070d8 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -12,6 +12,7 @@ import { loadProjectConfig, waitForPort, } from '../../operations/dev'; +import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js'; import { useEffect, useMemo, useRef, useState } from 'react'; type ServerStatus = 'starting' | 'running' | 'error' | 'stopped'; @@ -69,7 +70,10 @@ export function useDevServer(options: { workingDir: string; port: number; agentN // Load env vars from agentcore/.env if (root) { const vars = await readEnvFile(root); - setEnvVars(vars); + const gatewayEnvVars = await getGatewayEnvVars(); + // Gateway env vars go first, .env.local overrides take precedence + const mergedEnvVars = { ...gatewayEnvVars, ...vars }; + setEnvVars(mergedEnvVars); } setConfigLoaded(true); diff --git a/src/schema/schemas/deployed-state.ts b/src/schema/schemas/deployed-state.ts index be6efacd..b82b40a7 100644 --- a/src/schema/schemas/deployed-state.ts +++ b/src/schema/schemas/deployed-state.ts @@ -24,6 +24,7 @@ export type AgentCoreDeployedState = z.infer;