From b0fd996ab8989a6814996ede02090ea8816d7f08 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Mon, 9 Mar 2026 15:16:49 -0400 Subject: [PATCH 1/2] fix: wire identity OAuth and gateway auth CLI options through to primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/cli/primitives/CredentialPrimitive.tsx | 151 +++++++++++++-------- src/cli/primitives/GatewayPrimitive.ts | 16 +++ src/cli/tui/hooks/useCreateMcp.ts | 3 + 3 files changed, 117 insertions(+), 53 deletions(-) diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index 9005c4e6..a2f912ae 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -256,70 +256,115 @@ export class CredentialPrimitive extends BasePrimitive', 'Credential name [non-interactive]') .option('--api-key ', 'The API key value [non-interactive]') .option('--json', 'Output as JSON [non-interactive]') - .action(async (cliOptions: { name?: string; apiKey?: string; json?: boolean }) => { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + .option('--type ', 'Credential type: api-key (default) or oauth [non-interactive]') + .option('--discovery-url ', 'OAuth discovery URL [non-interactive]') + .option('--client-id ', 'OAuth client ID [non-interactive]') + .option('--client-secret ', 'OAuth client secret [non-interactive]') + .option('--scopes ', 'OAuth scopes, comma-separated [non-interactive]') + .action( + async (cliOptions: { + name?: string; + apiKey?: string; + json?: boolean; + type?: string; + discoveryUrl?: string; + clientId?: string; + clientSecret?: string; + scopes?: string; + }) => { + try { + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + if ( + cliOptions.name || + cliOptions.apiKey || + cliOptions.json || + cliOptions.type || + cliOptions.discoveryUrl || + cliOptions.clientId || + cliOptions.clientSecret || + cliOptions.scopes + ) { + // CLI mode + const validation = validateAddIdentityOptions({ + name: cliOptions.name, + type: cliOptions.type as 'api-key' | 'oauth' | undefined, + apiKey: cliOptions.apiKey, + discoveryUrl: cliOptions.discoveryUrl, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + scopes: cliOptions.scopes, + }); + + if (!validation.valid) { + if (cliOptions.json) { + console.log(JSON.stringify({ success: false, error: validation.error })); + } else { + console.error(validation.error); + } + process.exit(1); + } - if (cliOptions.name || cliOptions.apiKey || cliOptions.json) { - // CLI mode - const validation = validateAddIdentityOptions({ - name: cliOptions.name, - apiKey: cliOptions.apiKey, - }); + const addOptions = + cliOptions.type === 'oauth' + ? { + type: 'OAuthCredentialProvider' as const, + name: cliOptions.name!, + discoveryUrl: cliOptions.discoveryUrl!, + clientId: cliOptions.clientId!, + clientSecret: cliOptions.clientSecret!, + scopes: cliOptions.scopes + ?.split(',') + .map(s => s.trim()) + .filter(Boolean), + } + : { + type: 'ApiKeyCredentialProvider' as const, + name: cliOptions.name!, + apiKey: cliOptions.apiKey!, + }; + + const result = await this.add(addOptions); - if (!validation.valid) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); + console.log(JSON.stringify(result)); + } else if (result.success) { + console.log(`Added credential '${result.credentialName}'`); } else { - console.error(validation.error); + console.error(result.error); } - process.exit(1); + process.exit(result.success ? 0 : 1); + } else { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); } - - const result = await this.add({ - type: 'ApiKeyCredentialProvider', - name: cliOptions.name!, - apiKey: cliOptions.apiKey!, - }); - + } catch (error) { if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added credential '${result.credentialName}'`); + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); } else { - console.error(result.error); + console.error(getErrorMessage(error)); } - process.exit(result.success ? 0 : 1); - } else { - // TUI fallback — dynamic imports to avoid pulling ink (async) into registry - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(getErrorMessage(error)); + process.exit(1); } - process.exit(1); } - }); + ); this.registerRemoveSubcommand(removeCmd); } diff --git a/src/cli/primitives/GatewayPrimitive.ts b/src/cli/primitives/GatewayPrimitive.ts index 519e9471..93fc97e9 100644 --- a/src/cli/primitives/GatewayPrimitive.ts +++ b/src/cli/primitives/GatewayPrimitive.ts @@ -22,6 +22,9 @@ export interface AddGatewayOptions { discoveryUrl?: string; allowedAudience?: string; allowedClients?: string; + allowedScopes?: string; + agentClientId?: string; + agentClientSecret?: string; agents?: string; } @@ -179,6 +182,9 @@ export class GatewayPrimitive extends BasePrimitive s.trim()) .filter(Boolean) : [], + ...(options.allowedScopes + ? { + allowedScopes: options.allowedScopes + .split(',') + .map(s => s.trim()) + .filter(Boolean), + } + : {}), + ...(options.agentClientId ? { agentClientId: options.agentClientId } : {}), + ...(options.agentClientSecret ? { agentClientSecret: options.agentClientSecret } : {}), }; } diff --git a/src/cli/tui/hooks/useCreateMcp.ts b/src/cli/tui/hooks/useCreateMcp.ts index 52ac120f..c6fa1be4 100644 --- a/src/cli/tui/hooks/useCreateMcp.ts +++ b/src/cli/tui/hooks/useCreateMcp.ts @@ -25,6 +25,9 @@ export function useCreateGateway() { discoveryUrl: config.jwtConfig?.discoveryUrl, allowedAudience: config.jwtConfig?.allowedAudience?.join(','), allowedClients: config.jwtConfig?.allowedClients?.join(','), + allowedScopes: config.jwtConfig?.allowedScopes?.join(','), + agentClientId: config.jwtConfig?.agentClientId, + agentClientSecret: config.jwtConfig?.agentClientSecret, }); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create gateway'); From 439a5edeaf7e765420dc4bca8df9e721fadf7676 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Mon, 9 Mar 2026 15:17:18 -0400 Subject: [PATCH 2/2] 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 --- .../add/__tests__/add-gateway.test.ts | 45 ++++++++++++++++++ .../add/__tests__/add-identity.test.ts | 46 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/cli/commands/add/__tests__/add-gateway.test.ts b/src/cli/commands/add/__tests__/add-gateway.test.ts index 91c17100..50d0f784 100644 --- a/src/cli/commands/add/__tests__/add-gateway.test.ts +++ b/src/cli/commands/add/__tests__/add-gateway.test.ts @@ -150,5 +150,50 @@ describe('add gateway command', () => { expect(json.success).toBe(false); expect(json.error.includes('well-known'), `Error: ${json.error}`).toBeTruthy(); }); + + it('creates gateway with allowedScopes and agent credentials', async () => { + const gatewayName = `scopes-gw-${Date.now()}`; + const result = await runCLI( + [ + 'add', + 'gateway', + '--name', + gatewayName, + '--authorizer-type', + 'CUSTOM_JWT', + '--discovery-url', + 'https://example.com/.well-known/openid-configuration', + '--allowed-clients', + 'client1', + '--allowed-scopes', + 'scope1,scope2', + '--agent-client-id', + 'agent-cid', + '--agent-client-secret', + 'agent-secret', + '--json', + ], + projectDir + ); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + // Verify allowedScopes in mcp.json + const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); + const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + expect(gateway, 'Gateway should be in mcp.json').toBeTruthy(); + expect(gateway.authorizerType).toBe('CUSTOM_JWT'); + expect(gateway.authorizerConfiguration?.customJwtAuthorizer?.allowedScopes).toEqual(['scope1', 'scope2']); + + // Verify managed OAuth credential in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const credential = projectSpec.credentials.find((c: { name: string }) => c.name === `${gatewayName}-oauth`); + expect(credential, 'Managed OAuth credential should exist').toBeTruthy(); + expect(credential.type).toBe('OAuthCredentialProvider'); + expect(credential.managed).toBe(true); + expect(credential.usage).toBe('inbound'); + }); }); }); diff --git a/src/cli/commands/add/__tests__/add-identity.test.ts b/src/cli/commands/add/__tests__/add-identity.test.ts index 4c21d18d..b281486a 100644 --- a/src/cli/commands/add/__tests__/add-identity.test.ts +++ b/src/cli/commands/add/__tests__/add-identity.test.ts @@ -64,4 +64,50 @@ describe('add identity command', () => { expect(credential.type).toBe('ApiKeyCredentialProvider'); }); }); + + describe('oauth identity creation', () => { + it('creates OAuth credential with discovery URL and scopes', async () => { + const identityName = `oauth-${Date.now()}`; + const result = await runCLI( + [ + 'add', + 'identity', + '--type', + 'oauth', + '--name', + identityName, + '--discovery-url', + 'https://idp.example.com/.well-known/openid-configuration', + '--client-id', + 'my-client-id', + '--client-secret', + 'my-client-secret', + '--scopes', + 'read,write', + '--json', + ], + projectDir + ); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + expect(json.credentialName).toBe(identityName); + + // Verify in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const credential = projectSpec.credentials.find((c: { name: string }) => c.name === identityName); + expect(credential, 'Credential should be in project credentials').toBeTruthy(); + expect(credential.type).toBe('OAuthCredentialProvider'); + expect(credential.discoveryUrl).toBe('https://idp.example.com/.well-known/openid-configuration'); + expect(credential.vendor).toBe('CustomOauth2'); + expect(credential.scopes).toEqual(['read', 'write']); + + // Verify env vars in .env.local + const envContent = await readFile(join(projectDir, 'agentcore/.env.local'), 'utf-8'); + const envPrefix = `AGENTCORE_CREDENTIAL_${identityName.toUpperCase().replace(/-/g, '_')}`; + expect(envContent).toContain(`${envPrefix}_CLIENT_ID=`); + expect(envContent).toContain(`${envPrefix}_CLIENT_SECRET=`); + }); + }); });