Skip to content

Commit c9cca83

Browse files
jesseturner21claude
andcommitted
feat(import): extract custom JWT authorizer config from starter toolkit YAML
Instead of warning users to manually recreate the gateway, the import pipeline now reads authorizer_configuration.customJWTAuthorizer from the starter toolkit YAML and writes authorizerType/authorizerConfiguration to agentcore.json so CDK handles it automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d9257db commit c9cca83

7 files changed

Lines changed: 489 additions & 125 deletions

File tree

package-lock.json

Lines changed: 91 additions & 109 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"constructs": "^10.0.0"
103103
},
104104
"devDependencies": {
105-
"@aws-sdk/client-cognito-identity-provider": "^3.1017.0",
105+
"@aws-sdk/client-cognito-identity-provider": "^3.1018.0",
106106
"@eslint/js": "^9.39.2",
107107
"@modelcontextprotocol/sdk": "^1.0.0",
108108
"@secretlint/secretlint-rule-preset-recommend": "^11.3.0",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
default_agent: jwt_agent
2+
agents:
3+
jwt_agent:
4+
name: jwt_agent
5+
entrypoint: main.py
6+
deployment_type: direct_code_deploy
7+
runtime_type: PYTHON_3_12
8+
language: python
9+
aws:
10+
account: '111122223333'
11+
region: us-west-2
12+
network_configuration:
13+
network_mode: PUBLIC
14+
protocol_configuration:
15+
server_protocol: HTTP
16+
observability:
17+
enabled: true
18+
bedrock_agentcore:
19+
agent_id: null
20+
agent_arn: null
21+
memory:
22+
mode: NO_MEMORY
23+
authorizer_configuration:
24+
customJWTAuthorizer:
25+
discoveryUrl: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_abc123/.well-known/openid-configuration'
26+
allowedClients:
27+
- client-id-1
28+
- client-id-2
29+
allowedAudience:
30+
- aud-1
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/**
2+
* Tests for importing custom JWT authorizer configuration from starter toolkit YAML.
3+
*/
4+
import { parseStarterToolkitYaml } from '../yaml-parser.js';
5+
import * as fs from 'node:fs';
6+
import * as os from 'node:os';
7+
import * as path from 'node:path';
8+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9+
10+
// ---- Mocks ----
11+
12+
const mockReadProjectSpec = vi.fn();
13+
const mockWriteProjectSpec = vi.fn();
14+
const mockReadAWSDeploymentTargets = vi.fn();
15+
const mockWriteAWSDeploymentTargets = vi.fn();
16+
const mockReadDeployedState = vi.fn();
17+
const mockWriteDeployedState = vi.fn();
18+
const mockFindConfigRoot = vi.fn();
19+
20+
vi.mock('../../../../lib', () => ({
21+
APP_DIR: 'app',
22+
ConfigIO: class MockConfigIO {
23+
readProjectSpec = mockReadProjectSpec;
24+
writeProjectSpec = mockWriteProjectSpec;
25+
readAWSDeploymentTargets = mockReadAWSDeploymentTargets;
26+
writeAWSDeploymentTargets = mockWriteAWSDeploymentTargets;
27+
readDeployedState = mockReadDeployedState;
28+
writeDeployedState = mockWriteDeployedState;
29+
},
30+
findConfigRoot: (...args: unknown[]) => mockFindConfigRoot(...args),
31+
}));
32+
33+
vi.mock('../../../aws/account', () => ({
34+
validateAwsCredentials: vi.fn().mockResolvedValue(undefined),
35+
}));
36+
37+
vi.mock('../../../cdk/local-cdk-project', () => ({
38+
LocalCdkProject: vi.fn(),
39+
}));
40+
41+
vi.mock('../../../cdk/toolkit-lib', () => ({
42+
silentIoHost: {},
43+
}));
44+
45+
vi.mock('../../../logging', () => ({
46+
ExecLogger: class MockExecLogger {
47+
startStep = vi.fn();
48+
endStep = vi.fn();
49+
log = vi.fn();
50+
finalize = vi.fn();
51+
getRelativeLogPath = vi.fn().mockReturnValue('agentcore/.cli/logs/import/import-mock.log');
52+
logFilePath = 'agentcore/.cli/logs/import/import-mock.log';
53+
},
54+
}));
55+
56+
vi.mock('../../../operations/deploy', () => ({
57+
buildCdkProject: vi.fn(),
58+
synthesizeCdk: vi.fn(),
59+
}));
60+
61+
vi.mock('../../../operations/python/setup', () => ({
62+
setupPythonProject: vi.fn().mockResolvedValue({ status: 'success' }),
63+
}));
64+
65+
vi.mock('../phase1-update', () => ({
66+
executePhase1: vi.fn(),
67+
getDeployedTemplate: vi.fn(),
68+
}));
69+
70+
vi.mock('../phase2-import', () => ({
71+
executePhase2: vi.fn(),
72+
publishCdkAssets: vi.fn(),
73+
}));
74+
75+
// ============================================================================
76+
// YAML Parsing: JWT authorizer extraction
77+
// ============================================================================
78+
79+
describe('YAML parsing: JWT authorizer config', () => {
80+
it('extracts authorizerType and authorizerConfiguration from YAML with customJWTAuthorizer', () => {
81+
const fixturePath = path.join(__dirname, 'fixtures', 'jwt-authorizer.yaml');
82+
const parsed = parseStarterToolkitYaml(fixturePath);
83+
84+
expect(parsed.agents).toHaveLength(1);
85+
const agent = parsed.agents[0]!;
86+
expect(agent.authorizerType).toBe('CUSTOM_JWT');
87+
expect(agent.authorizerConfiguration).toBeDefined();
88+
expect(agent.authorizerConfiguration!.customJwtAuthorizer).toBeDefined();
89+
90+
const jwt = agent.authorizerConfiguration!.customJwtAuthorizer!;
91+
expect(jwt.discoveryUrl).toBe(
92+
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_abc123/.well-known/openid-configuration'
93+
);
94+
expect(jwt.allowedClients).toEqual(['client-id-1', 'client-id-2']);
95+
expect(jwt.allowedAudience).toEqual(['aud-1']);
96+
});
97+
98+
it('returns no authorizer fields when authorizer_configuration is null', () => {
99+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jwt-test-'));
100+
try {
101+
const yamlContent = `
102+
default_agent: test_agent
103+
agents:
104+
test_agent:
105+
name: test_agent
106+
entrypoint: main.py
107+
deployment_type: container
108+
aws:
109+
account: '111122223333'
110+
region: us-east-1
111+
network_configuration:
112+
network_mode: PUBLIC
113+
protocol_configuration:
114+
server_protocol: HTTP
115+
observability:
116+
enabled: true
117+
bedrock_agentcore:
118+
agent_id: null
119+
agent_arn: null
120+
memory:
121+
mode: NO_MEMORY
122+
authorizer_configuration: null
123+
`;
124+
const filePath = path.join(tmpDir, '.bedrock_agentcore.yaml');
125+
fs.writeFileSync(filePath, yamlContent);
126+
127+
const parsed = parseStarterToolkitYaml(filePath);
128+
const agent = parsed.agents[0]!;
129+
expect(agent.authorizerType).toBeUndefined();
130+
expect(agent.authorizerConfiguration).toBeUndefined();
131+
} finally {
132+
fs.rmSync(tmpDir, { recursive: true, force: true });
133+
}
134+
});
135+
136+
it('returns no authorizer fields when authorizer_configuration is absent', () => {
137+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jwt-test-'));
138+
try {
139+
const yamlContent = `
140+
default_agent: test_agent
141+
agents:
142+
test_agent:
143+
name: test_agent
144+
entrypoint: main.py
145+
deployment_type: container
146+
aws:
147+
account: '111122223333'
148+
region: us-east-1
149+
network_configuration:
150+
network_mode: PUBLIC
151+
protocol_configuration:
152+
server_protocol: HTTP
153+
observability:
154+
enabled: true
155+
bedrock_agentcore:
156+
agent_id: null
157+
agent_arn: null
158+
memory:
159+
mode: NO_MEMORY
160+
`;
161+
const filePath = path.join(tmpDir, '.bedrock_agentcore.yaml');
162+
fs.writeFileSync(filePath, yamlContent);
163+
164+
const parsed = parseStarterToolkitYaml(filePath);
165+
const agent = parsed.agents[0]!;
166+
expect(agent.authorizerType).toBeUndefined();
167+
expect(agent.authorizerConfiguration).toBeUndefined();
168+
} finally {
169+
fs.rmSync(tmpDir, { recursive: true, force: true });
170+
}
171+
});
172+
173+
it('handles customJWTAuthorizer with all optional fields', () => {
174+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jwt-test-'));
175+
try {
176+
const yamlContent = `
177+
default_agent: test_agent
178+
agents:
179+
test_agent:
180+
name: test_agent
181+
entrypoint: main.py
182+
deployment_type: container
183+
aws:
184+
account: '111122223333'
185+
region: us-east-1
186+
network_configuration:
187+
network_mode: PUBLIC
188+
protocol_configuration:
189+
server_protocol: HTTP
190+
observability:
191+
enabled: true
192+
bedrock_agentcore:
193+
agent_id: null
194+
agent_arn: null
195+
memory:
196+
mode: NO_MEMORY
197+
authorizer_configuration:
198+
customJWTAuthorizer:
199+
discoveryUrl: "https://example.com/.well-known/openid-configuration"
200+
allowedClients:
201+
- client1
202+
allowedAudience:
203+
- aud1
204+
- aud2
205+
allowedScopes:
206+
- read
207+
- write
208+
`;
209+
const filePath = path.join(tmpDir, '.bedrock_agentcore.yaml');
210+
fs.writeFileSync(filePath, yamlContent);
211+
212+
const parsed = parseStarterToolkitYaml(filePath);
213+
const jwt = parsed.agents[0]!.authorizerConfiguration!.customJwtAuthorizer!;
214+
expect(jwt.discoveryUrl).toBe('https://example.com/.well-known/openid-configuration');
215+
expect(jwt.allowedClients).toEqual(['client1']);
216+
expect(jwt.allowedAudience).toEqual(['aud1', 'aud2']);
217+
expect(jwt.allowedScopes).toEqual(['read', 'write']);
218+
} finally {
219+
fs.rmSync(tmpDir, { recursive: true, force: true });
220+
}
221+
});
222+
});
223+
224+
// ============================================================================
225+
// handleImport: authorizer passthrough to agentcore.json
226+
// ============================================================================
227+
228+
describe('handleImport: JWT authorizer passthrough', () => {
229+
let tmpDir: string;
230+
231+
beforeEach(() => {
232+
vi.clearAllMocks();
233+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jwt-import-'));
234+
235+
const projectDir = path.join(tmpDir, 'myproject');
236+
const configDir = path.join(projectDir, 'agentcore');
237+
fs.mkdirSync(configDir, { recursive: true });
238+
fs.writeFileSync(
239+
path.join(configDir, 'agentcore.json'),
240+
JSON.stringify({ name: 'myproject', version: 1, agents: [], memories: [], credentials: [] })
241+
);
242+
mockFindConfigRoot.mockReturnValue(configDir);
243+
});
244+
245+
afterEach(() => {
246+
fs.rmSync(tmpDir, { recursive: true, force: true });
247+
});
248+
249+
it('includes authorizerType and authorizerConfiguration in written spec', async () => {
250+
mockReadProjectSpec.mockResolvedValue({
251+
name: 'myproject',
252+
version: 1,
253+
agents: [],
254+
memories: [],
255+
credentials: [],
256+
});
257+
mockWriteProjectSpec.mockResolvedValue(undefined);
258+
mockReadAWSDeploymentTargets.mockResolvedValue([{ name: 'default', account: '111122223333', region: 'us-west-2' }]);
259+
260+
const fixturePath = path.join(__dirname, 'fixtures', 'jwt-authorizer.yaml');
261+
const { handleImport } = await import('../actions.js');
262+
const result = await handleImport({ source: fixturePath });
263+
264+
expect(result.success).toBe(true);
265+
expect(mockWriteProjectSpec).toHaveBeenCalledTimes(1);
266+
267+
const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0];
268+
const agent = writtenSpec.agents[0];
269+
expect(agent.authorizerType).toBe('CUSTOM_JWT');
270+
expect(agent.authorizerConfiguration).toBeDefined();
271+
expect(agent.authorizerConfiguration.customJwtAuthorizer.discoveryUrl).toBe(
272+
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_abc123/.well-known/openid-configuration'
273+
);
274+
expect(agent.authorizerConfiguration.customJwtAuthorizer.allowedClients).toEqual(['client-id-1', 'client-id-2']);
275+
});
276+
277+
it('does not include authorizer fields when YAML has no authorizer config', async () => {
278+
mockReadProjectSpec.mockResolvedValue({
279+
name: 'myproject',
280+
version: 1,
281+
agents: [],
282+
memories: [],
283+
credentials: [],
284+
});
285+
mockWriteProjectSpec.mockResolvedValue(undefined);
286+
mockReadAWSDeploymentTargets.mockResolvedValue([{ name: 'default', account: '111122223333', region: 'us-west-2' }]);
287+
288+
const fixturePath = path.join(__dirname, 'fixtures', 'two-agents.yaml');
289+
const { handleImport } = await import('../actions.js');
290+
await handleImport({ source: fixturePath });
291+
292+
const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0];
293+
for (const agent of writtenSpec.agents) {
294+
expect(agent.authorizerType).toBeUndefined();
295+
expect(agent.authorizerConfiguration).toBeUndefined();
296+
}
297+
});
298+
299+
it('does not emit authorizer warning for agents with JWT config', async () => {
300+
mockReadProjectSpec.mockResolvedValue({
301+
name: 'myproject',
302+
version: 1,
303+
agents: [],
304+
memories: [],
305+
credentials: [],
306+
});
307+
mockWriteProjectSpec.mockResolvedValue(undefined);
308+
mockReadAWSDeploymentTargets.mockResolvedValue([{ name: 'default', account: '111122223333', region: 'us-west-2' }]);
309+
310+
const fixturePath = path.join(__dirname, 'fixtures', 'jwt-authorizer.yaml');
311+
const { handleImport } = await import('../actions.js');
312+
313+
const progressMessages: string[] = [];
314+
await handleImport({
315+
source: fixturePath,
316+
onProgress: (msg: string) => progressMessages.push(msg),
317+
});
318+
319+
const authWarning = progressMessages.find(m => m.includes('not automatically imported'));
320+
expect(authWarning).toBeUndefined();
321+
});
322+
});

src/cli/commands/import/actions.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ function toAgentEnvSpec(agent: ParsedStarterToolkitConfig['agents'][0]): AgentEn
6262
spec.networkConfig = agent.networkConfig;
6363
}
6464

65+
if (agent.authorizerType) {
66+
spec.authorizerType = agent.authorizerType;
67+
}
68+
if (agent.authorizerConfiguration) {
69+
spec.authorizerConfiguration = agent.authorizerConfiguration;
70+
}
71+
6572
return spec;
6673
}
6774

@@ -298,16 +305,6 @@ export async function handleImport(options: ImportOptions): Promise<ImportResult
298305
}
299306
}
300307

301-
for (const agent of parsed.agents) {
302-
if (agent.hasAuthorizerConfig) {
303-
const warnMsg =
304-
`Warning: Agent "${agent.name}" has a custom JWT authorizer configured in the starter toolkit. ` +
305-
`This is not automatically imported. To recreate it, run: agentcore add gateway --authorizer-type CUSTOM_JWT`;
306-
logger.log(warnMsg, 'warn');
307-
onProgress?.(warnMsg);
308-
}
309-
}
310-
311308
const existingMemoryNames = new Set((projectSpec.memories ?? []).map(m => m.name));
312309
const newlyAddedMemoryNames = new Set<string>();
313310
for (const mem of parsed.memories) {

0 commit comments

Comments
 (0)