Skip to content

Commit 8a1af21

Browse files
authored
fix: pass requestHeaderAllowlist through create flow and fix tag command types (#643)
Two fixes: 1. The create flow was dropping requestHeaderAllowlist when building the GenerateConfig from AddAgentConfig. Headers entered through the TUI Advanced settings were collected correctly but never written to agentcore.json because two GenerateConfig construction sites omitted the field. Adds the missing field to useCreateFlow.ts (TUI create path) and create/action.ts (CLI create path). 2. The tag command referenced a missing ./types module and stale readMcpSpec/writeMcpSpec methods removed in the mcp.json merge (#605). Creates the types file and updates gateway tag operations to use the project spec instead.
1 parent eda0f5d commit 8a1af21

File tree

5 files changed

+442
-0
lines changed

5 files changed

+442
-0
lines changed

src/cli/commands/create/action.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export interface CreateWithAgentOptions {
136136
networkMode?: NetworkMode;
137137
subnets?: string[];
138138
securityGroups?: string[];
139+
requestHeaderAllowlist?: string[];
139140
agentId?: string;
140141
agentAliasId?: string;
141142
region?: string;
@@ -158,6 +159,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
158159
networkMode,
159160
subnets,
160161
securityGroups,
162+
requestHeaderAllowlist,
161163
skipGit,
162164
skipPythonSetup,
163165
onProgress,
@@ -234,6 +236,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
234236
networkMode,
235237
subnets,
236238
securityGroups,
239+
requestHeaderAllowlist,
237240
};
238241

239242
// Resolve credential strategy FIRST (new project has no existing credentials)
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { addTag, listTags, removeDefaultTag, removeTag, setDefaultTag } from '../action.js';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
const { mockReadProjectSpec, mockWriteProjectSpec, mockFindConfigRoot } = vi.hoisted(() => ({
5+
mockReadProjectSpec: vi.fn(),
6+
mockWriteProjectSpec: vi.fn(),
7+
mockFindConfigRoot: vi.fn(),
8+
}));
9+
10+
vi.mock('../../../../lib/index.js', () => ({
11+
ConfigIO: class {
12+
readProjectSpec = mockReadProjectSpec;
13+
writeProjectSpec = mockWriteProjectSpec;
14+
},
15+
findConfigRoot: mockFindConfigRoot,
16+
NoProjectError: class NoProjectError extends Error {
17+
constructor() {
18+
super('No AgentCore project found');
19+
this.name = 'NoProjectError';
20+
}
21+
},
22+
}));
23+
24+
const baseSpec = () => ({
25+
name: 'TestProject',
26+
version: 1,
27+
tags: { 'agentcore:created-by': 'agentcore-cli' },
28+
agents: [
29+
{
30+
type: 'AgentCoreRuntime',
31+
name: 'myAgent',
32+
build: 'CodeZip',
33+
entrypoint: 'main.py',
34+
codeLocation: 'app/myAgent',
35+
runtimeVersion: 'python3.13',
36+
protocol: 'HTTP',
37+
},
38+
],
39+
memories: [{ type: 'AgentCoreMemory', name: 'myMemory', eventExpiryDuration: 30, strategies: [] }],
40+
credentials: [],
41+
agentCoreGateways: [
42+
{
43+
name: 'myGateway',
44+
targets: [],
45+
authorizerType: 'NONE',
46+
enableSemanticSearch: true,
47+
exceptionLevel: 'NONE',
48+
},
49+
],
50+
});
51+
52+
beforeEach(() => {
53+
vi.clearAllMocks();
54+
mockFindConfigRoot.mockReturnValue('/fake/config/root');
55+
mockReadProjectSpec.mockResolvedValue(baseSpec());
56+
mockWriteProjectSpec.mockResolvedValue(undefined);
57+
});
58+
59+
describe('listTags', () => {
60+
it('returns project defaults and all resources with merged tags', async () => {
61+
const result = await listTags();
62+
expect(result.projectDefaults).toEqual({ 'agentcore:created-by': 'agentcore-cli' });
63+
expect(result.resources).toHaveLength(3);
64+
expect(result.resources[0]).toEqual({
65+
type: 'agent',
66+
name: 'myAgent',
67+
tags: { 'agentcore:created-by': 'agentcore-cli' },
68+
});
69+
});
70+
71+
it('filters by resource ref', async () => {
72+
const result = await listTags('agent:myAgent');
73+
expect(result.resources).toHaveLength(1);
74+
expect(result.resources[0]!.name).toBe('myAgent');
75+
});
76+
77+
it('throws on nonexistent resource', async () => {
78+
await expect(listTags('agent:nonexistent')).rejects.toThrow('not found');
79+
});
80+
});
81+
82+
describe('addTag', () => {
83+
it('adds tag to agent and writes spec', async () => {
84+
const result = await addTag('agent:myAgent', 'env', 'prod');
85+
expect(result.success).toBe(true);
86+
expect(mockWriteProjectSpec).toHaveBeenCalledTimes(1);
87+
const written = mockWriteProjectSpec.mock.calls[0]![0];
88+
expect(written.agents[0].tags).toEqual({ env: 'prod' });
89+
});
90+
91+
it('adds tag to gateway and writes project spec', async () => {
92+
const result = await addTag('gateway:myGateway', 'env', 'prod');
93+
expect(result.success).toBe(true);
94+
expect(mockWriteProjectSpec).toHaveBeenCalledTimes(1);
95+
const written = mockWriteProjectSpec.mock.calls[0]![0];
96+
expect(written.agentCoreGateways[0].tags).toEqual({ env: 'prod' });
97+
});
98+
99+
it('throws for invalid resource ref', async () => {
100+
await expect(addTag('invalid', 'key', 'value')).rejects.toThrow('Invalid resource reference');
101+
});
102+
103+
it('throws for nonexistent resource', async () => {
104+
await expect(addTag('agent:noSuchAgent', 'key', 'value')).rejects.toThrow('not found');
105+
});
106+
107+
it('rejects empty tag key', async () => {
108+
await expect(addTag('agent:myAgent', '', 'value')).rejects.toThrow('Invalid tag key');
109+
});
110+
111+
it('rejects tag key exceeding 128 chars', async () => {
112+
await expect(addTag('agent:myAgent', 'k'.repeat(129), 'value')).rejects.toThrow('Invalid tag key');
113+
});
114+
115+
it('rejects tag value exceeding 256 chars', async () => {
116+
await expect(addTag('agent:myAgent', 'key', 'v'.repeat(257))).rejects.toThrow('Invalid tag value');
117+
});
118+
119+
it('rejects tag key with invalid characters', async () => {
120+
await expect(addTag('agent:myAgent', 'key\x00bad', 'value')).rejects.toThrow('Invalid tag key');
121+
});
122+
123+
it('rejects tag value with invalid characters', async () => {
124+
await expect(addTag('agent:myAgent', 'key', 'value\x00bad')).rejects.toThrow('Invalid tag value');
125+
});
126+
127+
it('rejects agentcore: prefixed keys', async () => {
128+
await expect(addTag('agent:myAgent', 'agentcore:custom', 'value')).rejects.toThrow('managed by the system');
129+
});
130+
});
131+
132+
describe('removeTag', () => {
133+
it('removes tag from agent', async () => {
134+
const spec = baseSpec();
135+
(spec.agents[0] as Record<string, unknown>).tags = { env: 'prod', team: 'a' };
136+
mockReadProjectSpec.mockResolvedValue(spec);
137+
138+
const result = await removeTag('agent:myAgent', 'env');
139+
expect(result.success).toBe(true);
140+
const written = mockWriteProjectSpec.mock.calls[0]![0];
141+
expect(written.agents[0].tags).toEqual({ team: 'a' });
142+
});
143+
144+
it('throws when key not found with hint about defaults', async () => {
145+
await expect(removeTag('agent:myAgent', 'nonexistent')).rejects.toThrow('remove-defaults');
146+
});
147+
148+
it('rejects agentcore: prefixed keys', async () => {
149+
await expect(removeTag('agent:myAgent', 'agentcore:created-by')).rejects.toThrow('managed by the system');
150+
});
151+
});
152+
153+
describe('setDefaultTag', () => {
154+
it('sets project-level default tag', async () => {
155+
const result = await setDefaultTag('team', 'platform');
156+
expect(result.success).toBe(true);
157+
const written = mockWriteProjectSpec.mock.calls[0]![0];
158+
expect(written.tags).toEqual({ 'agentcore:created-by': 'agentcore-cli', team: 'platform' });
159+
});
160+
161+
it('rejects agentcore: prefixed keys', async () => {
162+
await expect(setDefaultTag('agentcore:custom', 'value')).rejects.toThrow('managed by the system');
163+
});
164+
});
165+
166+
describe('removeDefaultTag', () => {
167+
it('rejects removal of agentcore: prefixed system tags', async () => {
168+
await expect(removeDefaultTag('agentcore:created-by')).rejects.toThrow('managed by the system');
169+
});
170+
171+
it('removes user-defined project-level default tag', async () => {
172+
const spec = baseSpec();
173+
(spec.tags as Record<string, string>).team = 'platform';
174+
mockReadProjectSpec.mockResolvedValue(spec);
175+
176+
const result = await removeDefaultTag('team');
177+
expect(result.success).toBe(true);
178+
const written = mockWriteProjectSpec.mock.calls[0]![0];
179+
expect(written.tags).toEqual({ 'agentcore:created-by': 'agentcore-cli' });
180+
});
181+
182+
it('throws when key not found', async () => {
183+
await expect(removeDefaultTag('nonexistent')).rejects.toThrow('not found');
184+
});
185+
});

0 commit comments

Comments
 (0)