diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 474ec6c3..5a99f3a8 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,9 +2,9 @@ name: Build and Test on: push: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] pull_request: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5eee5e90..a64f5bb3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,11 +2,11 @@ name: CodeQL on: push: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] pull_request: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] pull_request_target: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] # Cancel in-progress runs when a new commit is pushed concurrency: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index edde6245..557f787b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -6,7 +6,7 @@ on: description: 'AWS region for deployment' default: 'us-east-1' pull_request_target: - branches: [main] + branches: [main, feat/gateway-integration] permissions: id-token: write # OIDC — lets GitHub assume an AWS IAM role via short-lived token (no stored keys) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 377464de..b88258fe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Quality and Safety Checks on: push: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] pull_request: - branches: ['main'] + branches: ['main', 'feat/gateway-integration'] permissions: contents: read diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml index 1982c05f..e172e99f 100644 --- a/.github/workflows/pr-size.yml +++ b/.github/workflows/pr-size.yml @@ -4,7 +4,7 @@ name: PR Size Check and Label # Safe because this workflow only reads PR metadata — it never checks out untrusted code. on: pull_request_target: - branches: [main] + branches: [main, feat/gateway-integration] jobs: label-size: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 46fd69bf..02ed5fa7 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -2,7 +2,7 @@ name: Validate PR Title on: pull_request_target: - branches: [main] + branches: [main, feat/gateway-integration] types: [opened, edited, synchronize, reopened] jobs: diff --git a/src/cli/operations/mcp/create-mcp.ts b/src/cli/operations/mcp/create-mcp.ts index a03543e5..5f4447e8 100644 --- a/src/cli/operations/mcp/create-mcp.ts +++ b/src/cli/operations/mcp/create-mcp.ts @@ -126,7 +126,7 @@ export async function getExistingToolNames(): Promise { // Gateway targets for (const gateway of mcpSpec.agentCoreGateways) { for (const target of gateway.targets) { - for (const toolDef of target.toolDefinitions) { + for (const toolDef of target.toolDefinitions ?? []) { toolNames.push(toolDef.name); } } @@ -256,7 +256,7 @@ export async function createToolFromWizard(config: AddMcpToolConfig): Promise t.name === toolDef.name)) { + if ((existingTarget.toolDefinitions ?? []).some(t => t.name === toolDef.name)) { throw new Error(`Tool "${toolDef.name}" already exists in gateway "${gateway.name}".`); } } diff --git a/src/cli/operations/remove/remove-mcp-tool.ts b/src/cli/operations/remove/remove-mcp-tool.ts index 9b02be84..dd4dab28 100644 --- a/src/cli/operations/remove/remove-mcp-tool.ts +++ b/src/cli/operations/remove/remove-mcp-tool.ts @@ -109,7 +109,7 @@ export async function previewRemoveMcpTool(tool: RemovableMcpTool): Promise g.name === tool.gatewayName); const target = gateway?.targets.find(t => t.name === tool.name); if (target) { - for (const toolDef of target.toolDefinitions) { + for (const toolDef of target.toolDefinitions ?? []) { toolNamesToRemove.push(toolDef.name); } } diff --git a/src/cli/tui/screens/mcp/useAddGatewayWizard.ts b/src/cli/tui/screens/mcp/useAddGatewayWizard.ts index 48c1f0b4..136c8899 100644 --- a/src/cli/tui/screens/mcp/useAddGatewayWizard.ts +++ b/src/cli/tui/screens/mcp/useAddGatewayWizard.ts @@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react'; /** Maps authorizer type to the next step after authorizer selection */ const AUTHORIZER_NEXT_STEP: Record = { NONE: 'agents', + AWS_IAM: 'agents', CUSTOM_JWT: 'jwt-config', }; diff --git a/src/schema/schemas/__tests__/mcp.test.ts b/src/schema/schemas/__tests__/mcp.test.ts index cd437fd2..2414406b 100644 --- a/src/schema/schemas/__tests__/mcp.test.ts +++ b/src/schema/schemas/__tests__/mcp.test.ts @@ -261,6 +261,11 @@ describe('AgentCoreGatewayTargetSchema', () => { name: 'myTarget', targetType: 'lambda', toolDefinitions: [validToolDef], + compute: { + host: 'Lambda', + implementation: { language: 'Python', path: 'tools', handler: 'h' }, + pythonVersion: 'PYTHON_3_12', + }, }); expect(result.success).toBe(true); }); @@ -270,6 +275,11 @@ describe('AgentCoreGatewayTargetSchema', () => { name: 'myTarget', targetType: 'lambda', toolDefinitions: [], + compute: { + host: 'Lambda', + implementation: { language: 'Python', path: 'tools', handler: 'h' }, + pythonVersion: 'PYTHON_3_12', + }, }); expect(result.success).toBe(false); }); @@ -303,6 +313,11 @@ describe('AgentCoreGatewaySchema', () => { name: 'target1', targetType: 'lambda', toolDefinitions: [validToolDef], + compute: { + host: 'Lambda', + implementation: { language: 'Python', path: 'tools', handler: 'h' }, + pythonVersion: 'PYTHON_3_12', + }, }, ], }; @@ -387,7 +402,18 @@ describe('AgentCoreMcpSpecSchema', () => { agentCoreGateways: [ { name: 'gw1', - targets: [{ name: 't1', targetType: 'lambda', toolDefinitions: [validToolDef] }], + targets: [ + { + name: 't1', + targetType: 'lambda', + toolDefinitions: [validToolDef], + compute: { + host: 'Lambda', + implementation: { language: 'Python', path: 'tools', handler: 'h' }, + pythonVersion: 'PYTHON_3_12', + }, + }, + ], }, ], }); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 49a7f576..13f8241f 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -71,9 +71,6 @@ export type Memory = z.infer; // Credential Schema // ============================================================================ -export const CredentialTypeSchema = z.literal('ApiKeyCredentialProvider'); -export type CredentialType = z.infer; - export const CredentialNameSchema = z .string() .min(3, 'Credential name must be at least 3 characters') @@ -83,11 +80,33 @@ export const CredentialNameSchema = z 'Must contain only alphanumeric characters, underscores, dots, and hyphens (3-255 chars)' ); -export const CredentialSchema = z.object({ - type: CredentialTypeSchema, +export const CredentialTypeSchema = z.enum(['ApiKeyCredentialProvider', 'OAuthCredentialProvider']); +export type CredentialType = z.infer; + +export const ApiKeyCredentialSchema = z.object({ + type: z.literal('ApiKeyCredentialProvider'), + name: CredentialNameSchema, +}); + +export type ApiKeyCredential = z.infer; + +export const OAuthCredentialSchema = z.object({ + type: z.literal('OAuthCredentialProvider'), name: CredentialNameSchema, + /** OIDC discovery URL for the OAuth provider */ + discoveryUrl: z.string().url(), + /** Scopes this credential provider supports */ + scopes: z.array(z.string()).optional(), + /** Credential provider vendor type */ + vendor: z.string().default('CustomOauth2'), + /** Whether this credential was auto-created by the CLI (e.g., for CUSTOM_JWT inbound auth) */ + managed: z.boolean().optional(), }); +export type OAuthCredential = z.infer; + +export const CredentialSchema = z.discriminatedUnion('type', [ApiKeyCredentialSchema, OAuthCredentialSchema]); + export type Credential = z.infer; // ============================================================================ diff --git a/src/schema/schemas/mcp.ts b/src/schema/schemas/mcp.ts index e3890df1..f529da4a 100644 --- a/src/schema/schemas/mcp.ts +++ b/src/schema/schemas/mcp.ts @@ -15,7 +15,7 @@ export type GatewayTargetType = z.infer; // Gateway Authorization Schemas // ============================================================================ -export const GatewayAuthorizerTypeSchema = z.enum(['NONE', 'CUSTOM_JWT']); +export const GatewayAuthorizerTypeSchema = z.enum(['NONE', 'AWS_IAM', 'CUSTOM_JWT']); export type GatewayAuthorizerType = z.infer; /** OIDC well-known configuration endpoint suffix (per OpenID Connect Discovery 1.0 spec) */ @@ -44,6 +44,7 @@ export const CustomJwtAuthorizerConfigSchema = z.object({ allowedAudience: z.array(z.string().min(1)), /** List of allowed client IDs */ allowedClients: z.array(z.string().min(1)).min(1), + allowedScopes: z.array(z.string().min(1)).optional(), }); export type CustomJwtAuthorizerConfig = z.infer; @@ -57,6 +58,19 @@ export const GatewayAuthorizerConfigSchema = z.object({ export type GatewayAuthorizerConfig = z.infer; +export const OutboundAuthTypeSchema = z.enum(['OAUTH', 'API_KEY', 'NONE']); +export type OutboundAuthType = z.infer; + +export const OutboundAuthSchema = z + .object({ + type: OutboundAuthTypeSchema.default('NONE'), + credentialName: z.string().min(1).optional(), + scopes: z.array(z.string()).optional(), + }) + .strict(); + +export type OutboundAuth = z.infer; + export const McpImplLanguageSchema = z.enum(['TypeScript', 'Python']); export type McpImplementationLanguage = z.infer; @@ -262,10 +276,45 @@ export const AgentCoreGatewayTargetSchema = z .object({ name: z.string().min(1), targetType: GatewayTargetTypeSchema, - toolDefinitions: z.array(ToolDefinitionSchema).min(1), + /** Tool definitions. Required for Lambda targets. Optional for MCP Server (discovered via tools/list). */ + toolDefinitions: z.array(ToolDefinitionSchema).optional(), + /** Compute configuration. Required for Lambda/Runtime scaffold targets. */ compute: ToolComputeConfigSchema.optional(), + /** MCP Server endpoint URL. Required for external MCP Server targets. */ + endpoint: z.string().url().optional(), + /** Outbound auth configuration for the target. */ + outboundAuth: OutboundAuthSchema.optional(), }) - .strict(); + .strict() + .superRefine((data, ctx) => { + if (data.targetType === 'mcpServer' && !data.compute && !data.endpoint) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'MCP Server targets require either an endpoint URL or compute configuration.', + }); + } + if (data.targetType === 'lambda' && !data.compute) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Lambda targets require compute configuration.', + path: ['compute'], + }); + } + if (data.targetType === 'lambda' && (!data.toolDefinitions || data.toolDefinitions.length === 0)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Lambda targets require at least one tool definition.', + path: ['toolDefinitions'], + }); + } + if (data.outboundAuth && data.outboundAuth.type !== 'NONE' && !data.outboundAuth.credentialName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${data.outboundAuth.type} outbound auth requires a credentialName.`, + path: ['outboundAuth', 'credentialName'], + }); + } + }); export type AgentCoreGatewayTarget = z.infer;