Skip to content

Commit 32064ee

Browse files
authored
fix: wire identity OAuth and gateway auth CLI options through to primitives (#522)
* fix: wire identity OAuth and gateway auth CLI options through to primitives - CredentialPrimitive: register --type, --discovery-url, --client-id, --client-secret, --scopes CLI options and route OAuth vs API key in the action handler - GatewayPrimitive: add allowedScopes, agentClientId, agentClientSecret to AddGatewayOptions interface, pass them in the action handler, and map them in buildGatewayConfig() - useCreateMcp: pass allowedScopes, agentClientId, agentClientSecret from TUI jwtConfig through to gatewayPrimitive.add() These fields were documented, supported in TUI wizards, validated, and handled by downstream code — but never wired through the CLI option registration or the primitive plumbing layer. The identity bug caused Commander.js to reject OAuth flags outright. The gateway bug silently dropped the values. * test: add integration tests for identity OAuth and gateway auth CLI flows - add-identity.test.ts: verify OAuth credential creation via CLI with --type oauth, --discovery-url, --client-id, --client-secret, --scopes persists OAuthCredentialProvider to agentcore.json and env vars to .env.local - add-gateway.test.ts: verify --allowed-scopes and --agent-client-id/ --agent-client-secret persist allowedScopes to mcp.json and create managed OAuth credential in agentcore.json
1 parent fb6a4f7 commit 32064ee

File tree

5 files changed

+208
-53
lines changed

5 files changed

+208
-53
lines changed

src/cli/commands/add/__tests__/add-gateway.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,5 +150,50 @@ describe('add gateway command', () => {
150150
expect(json.success).toBe(false);
151151
expect(json.error.includes('well-known'), `Error: ${json.error}`).toBeTruthy();
152152
});
153+
154+
it('creates gateway with allowedScopes and agent credentials', async () => {
155+
const gatewayName = `scopes-gw-${Date.now()}`;
156+
const result = await runCLI(
157+
[
158+
'add',
159+
'gateway',
160+
'--name',
161+
gatewayName,
162+
'--authorizer-type',
163+
'CUSTOM_JWT',
164+
'--discovery-url',
165+
'https://example.com/.well-known/openid-configuration',
166+
'--allowed-clients',
167+
'client1',
168+
'--allowed-scopes',
169+
'scope1,scope2',
170+
'--agent-client-id',
171+
'agent-cid',
172+
'--agent-client-secret',
173+
'agent-secret',
174+
'--json',
175+
],
176+
projectDir
177+
);
178+
179+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
180+
const json = JSON.parse(result.stdout);
181+
expect(json.success).toBe(true);
182+
183+
// Verify allowedScopes in mcp.json
184+
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
185+
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
186+
expect(gateway, 'Gateway should be in mcp.json').toBeTruthy();
187+
expect(gateway.authorizerType).toBe('CUSTOM_JWT');
188+
expect(gateway.authorizerConfiguration?.customJwtAuthorizer?.allowedScopes).toEqual(['scope1', 'scope2']);
189+
190+
// Verify managed OAuth credential in agentcore.json
191+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
192+
const credential = projectSpec.credentials.find((c: { name: string }) => c.name === `${gatewayName}-oauth`);
193+
expect(credential, 'Managed OAuth credential should exist').toBeTruthy();
194+
expect(credential.type).toBe('OAuthCredentialProvider');
195+
expect(credential.managed).toBe(true);
196+
expect(credential.usage).toBe('inbound');
197+
});
153198
});
154199
});

src/cli/commands/add/__tests__/add-identity.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,50 @@ describe('add identity command', () => {
6464
expect(credential.type).toBe('ApiKeyCredentialProvider');
6565
});
6666
});
67+
68+
describe('oauth identity creation', () => {
69+
it('creates OAuth credential with discovery URL and scopes', async () => {
70+
const identityName = `oauth-${Date.now()}`;
71+
const result = await runCLI(
72+
[
73+
'add',
74+
'identity',
75+
'--type',
76+
'oauth',
77+
'--name',
78+
identityName,
79+
'--discovery-url',
80+
'https://idp.example.com/.well-known/openid-configuration',
81+
'--client-id',
82+
'my-client-id',
83+
'--client-secret',
84+
'my-client-secret',
85+
'--scopes',
86+
'read,write',
87+
'--json',
88+
],
89+
projectDir
90+
);
91+
92+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
93+
const json = JSON.parse(result.stdout);
94+
expect(json.success).toBe(true);
95+
expect(json.credentialName).toBe(identityName);
96+
97+
// Verify in agentcore.json
98+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
99+
const credential = projectSpec.credentials.find((c: { name: string }) => c.name === identityName);
100+
expect(credential, 'Credential should be in project credentials').toBeTruthy();
101+
expect(credential.type).toBe('OAuthCredentialProvider');
102+
expect(credential.discoveryUrl).toBe('https://idp.example.com/.well-known/openid-configuration');
103+
expect(credential.vendor).toBe('CustomOauth2');
104+
expect(credential.scopes).toEqual(['read', 'write']);
105+
106+
// Verify env vars in .env.local
107+
const envContent = await readFile(join(projectDir, 'agentcore/.env.local'), 'utf-8');
108+
const envPrefix = `AGENTCORE_CREDENTIAL_${identityName.toUpperCase().replace(/-/g, '_')}`;
109+
expect(envContent).toContain(`${envPrefix}_CLIENT_ID=`);
110+
expect(envContent).toContain(`${envPrefix}_CLIENT_SECRET=`);
111+
});
112+
});
67113
});

src/cli/primitives/CredentialPrimitive.tsx

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -256,70 +256,115 @@ export class CredentialPrimitive extends BasePrimitive<AddCredentialOptions, Rem
256256
.option('--name <name>', 'Credential name [non-interactive]')
257257
.option('--api-key <key>', 'The API key value [non-interactive]')
258258
.option('--json', 'Output as JSON [non-interactive]')
259-
.action(async (cliOptions: { name?: string; apiKey?: string; json?: boolean }) => {
260-
try {
261-
if (!findConfigRoot()) {
262-
console.error('No agentcore project found. Run `agentcore create` first.');
263-
process.exit(1);
264-
}
259+
.option('--type <type>', 'Credential type: api-key (default) or oauth [non-interactive]')
260+
.option('--discovery-url <url>', 'OAuth discovery URL [non-interactive]')
261+
.option('--client-id <id>', 'OAuth client ID [non-interactive]')
262+
.option('--client-secret <secret>', 'OAuth client secret [non-interactive]')
263+
.option('--scopes <scopes>', 'OAuth scopes, comma-separated [non-interactive]')
264+
.action(
265+
async (cliOptions: {
266+
name?: string;
267+
apiKey?: string;
268+
json?: boolean;
269+
type?: string;
270+
discoveryUrl?: string;
271+
clientId?: string;
272+
clientSecret?: string;
273+
scopes?: string;
274+
}) => {
275+
try {
276+
if (!findConfigRoot()) {
277+
console.error('No agentcore project found. Run `agentcore create` first.');
278+
process.exit(1);
279+
}
280+
281+
if (
282+
cliOptions.name ||
283+
cliOptions.apiKey ||
284+
cliOptions.json ||
285+
cliOptions.type ||
286+
cliOptions.discoveryUrl ||
287+
cliOptions.clientId ||
288+
cliOptions.clientSecret ||
289+
cliOptions.scopes
290+
) {
291+
// CLI mode
292+
const validation = validateAddIdentityOptions({
293+
name: cliOptions.name,
294+
type: cliOptions.type as 'api-key' | 'oauth' | undefined,
295+
apiKey: cliOptions.apiKey,
296+
discoveryUrl: cliOptions.discoveryUrl,
297+
clientId: cliOptions.clientId,
298+
clientSecret: cliOptions.clientSecret,
299+
scopes: cliOptions.scopes,
300+
});
301+
302+
if (!validation.valid) {
303+
if (cliOptions.json) {
304+
console.log(JSON.stringify({ success: false, error: validation.error }));
305+
} else {
306+
console.error(validation.error);
307+
}
308+
process.exit(1);
309+
}
265310

266-
if (cliOptions.name || cliOptions.apiKey || cliOptions.json) {
267-
// CLI mode
268-
const validation = validateAddIdentityOptions({
269-
name: cliOptions.name,
270-
apiKey: cliOptions.apiKey,
271-
});
311+
const addOptions =
312+
cliOptions.type === 'oauth'
313+
? {
314+
type: 'OAuthCredentialProvider' as const,
315+
name: cliOptions.name!,
316+
discoveryUrl: cliOptions.discoveryUrl!,
317+
clientId: cliOptions.clientId!,
318+
clientSecret: cliOptions.clientSecret!,
319+
scopes: cliOptions.scopes
320+
?.split(',')
321+
.map(s => s.trim())
322+
.filter(Boolean),
323+
}
324+
: {
325+
type: 'ApiKeyCredentialProvider' as const,
326+
name: cliOptions.name!,
327+
apiKey: cliOptions.apiKey!,
328+
};
329+
330+
const result = await this.add(addOptions);
272331

273-
if (!validation.valid) {
274332
if (cliOptions.json) {
275-
console.log(JSON.stringify({ success: false, error: validation.error }));
333+
console.log(JSON.stringify(result));
334+
} else if (result.success) {
335+
console.log(`Added credential '${result.credentialName}'`);
276336
} else {
277-
console.error(validation.error);
337+
console.error(result.error);
278338
}
279-
process.exit(1);
339+
process.exit(result.success ? 0 : 1);
340+
} else {
341+
// TUI fallback — dynamic imports to avoid pulling ink (async) into registry
342+
const [{ render }, { default: React }, { AddFlow }] = await Promise.all([
343+
import('ink'),
344+
import('react'),
345+
import('../tui/screens/add/AddFlow'),
346+
]);
347+
const { clear, unmount } = render(
348+
React.createElement(AddFlow, {
349+
isInteractive: false,
350+
onExit: () => {
351+
clear();
352+
unmount();
353+
process.exit(0);
354+
},
355+
})
356+
);
280357
}
281-
282-
const result = await this.add({
283-
type: 'ApiKeyCredentialProvider',
284-
name: cliOptions.name!,
285-
apiKey: cliOptions.apiKey!,
286-
});
287-
358+
} catch (error) {
288359
if (cliOptions.json) {
289-
console.log(JSON.stringify(result));
290-
} else if (result.success) {
291-
console.log(`Added credential '${result.credentialName}'`);
360+
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
292361
} else {
293-
console.error(result.error);
362+
console.error(getErrorMessage(error));
294363
}
295-
process.exit(result.success ? 0 : 1);
296-
} else {
297-
// TUI fallback — dynamic imports to avoid pulling ink (async) into registry
298-
const [{ render }, { default: React }, { AddFlow }] = await Promise.all([
299-
import('ink'),
300-
import('react'),
301-
import('../tui/screens/add/AddFlow'),
302-
]);
303-
const { clear, unmount } = render(
304-
React.createElement(AddFlow, {
305-
isInteractive: false,
306-
onExit: () => {
307-
clear();
308-
unmount();
309-
process.exit(0);
310-
},
311-
})
312-
);
313-
}
314-
} catch (error) {
315-
if (cliOptions.json) {
316-
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
317-
} else {
318-
console.error(getErrorMessage(error));
364+
process.exit(1);
319365
}
320-
process.exit(1);
321366
}
322-
});
367+
);
323368

324369
this.registerRemoveSubcommand(removeCmd);
325370
}

src/cli/primitives/GatewayPrimitive.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface AddGatewayOptions {
2222
discoveryUrl?: string;
2323
allowedAudience?: string;
2424
allowedClients?: string;
25+
allowedScopes?: string;
26+
agentClientId?: string;
27+
agentClientSecret?: string;
2528
agents?: string;
2629
}
2730

@@ -179,6 +182,9 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
179182
discoveryUrl: cliOptions.discoveryUrl,
180183
allowedAudience: cliOptions.allowedAudience,
181184
allowedClients: cliOptions.allowedClients,
185+
allowedScopes: cliOptions.allowedScopes,
186+
agentClientId: cliOptions.agentClientId,
187+
agentClientSecret: cliOptions.agentClientSecret,
182188
agents: cliOptions.agents,
183189
});
184190

@@ -293,6 +299,16 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
293299
.map(s => s.trim())
294300
.filter(Boolean)
295301
: [],
302+
...(options.allowedScopes
303+
? {
304+
allowedScopes: options.allowedScopes
305+
.split(',')
306+
.map(s => s.trim())
307+
.filter(Boolean),
308+
}
309+
: {}),
310+
...(options.agentClientId ? { agentClientId: options.agentClientId } : {}),
311+
...(options.agentClientSecret ? { agentClientSecret: options.agentClientSecret } : {}),
296312
};
297313
}
298314

src/cli/tui/hooks/useCreateMcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export function useCreateGateway() {
2525
discoveryUrl: config.jwtConfig?.discoveryUrl,
2626
allowedAudience: config.jwtConfig?.allowedAudience?.join(','),
2727
allowedClients: config.jwtConfig?.allowedClients?.join(','),
28+
allowedScopes: config.jwtConfig?.allowedScopes?.join(','),
29+
agentClientId: config.jwtConfig?.agentClientId,
30+
agentClientSecret: config.jwtConfig?.agentClientSecret,
2831
});
2932
if (!addResult.success) {
3033
throw new Error(addResult.error ?? 'Failed to create gateway');

0 commit comments

Comments
 (0)