Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions src/cli/commands/add/tool-action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConfigIO } from '../../../lib';
import type { HarnessSpec } from '../../../schema';
import type { HarnessGatewayOutboundAuth, HarnessSpec } from '../../../schema';
import type { HarnessToolType } from '../../../schema/schemas/primitives/harness';

export interface AddToolOptions {
Expand All @@ -11,9 +11,17 @@ export interface AddToolOptions {
codeInterpreterArn?: string;
gatewayArn?: string;
gateway?: string;
outboundAuth?: string;
providerArn?: string;
scopes?: string;
grantType?: string;
json?: boolean;
}

const VALID_OUTBOUND_AUTH_TYPES = ['awsIam', 'none', 'oauth'] as const;
const VALID_GRANT_TYPES = ['CLIENT_CREDENTIALS', 'USER_FEDERATION'] as const;
const ARN_PATTERN = /^arn:[^:]+:/;

export interface AddToolResult {
success: boolean;
error?: string;
Expand Down Expand Up @@ -49,6 +57,61 @@ export async function handleAddTool(options: AddToolOptions): Promise<AddToolRes
return { success: false, error: '--gateway-arn or --gateway is required for agentcore_gateway tools' };
}

let outboundAuth: HarnessGatewayOutboundAuth | undefined;
if (options.outboundAuth !== undefined) {
if (toolType !== 'agentcore_gateway') {
return { success: false, error: '--outbound-auth is only valid for agentcore_gateway tools' };
}
if (!VALID_OUTBOUND_AUTH_TYPES.includes(options.outboundAuth as (typeof VALID_OUTBOUND_AUTH_TYPES)[number])) {
return {
success: false,
error: `Invalid --outbound-auth '${options.outboundAuth}'. Valid: ${VALID_OUTBOUND_AUTH_TYPES.join(', ')}`,
};
}
if (options.outboundAuth === 'awsIam' || options.outboundAuth === 'none') {
if (options.providerArn || options.scopes || options.grantType) {
return {
success: false,
error: '--provider-arn, --scopes, and --grant-type are only valid with --outbound-auth oauth',
};
}
outboundAuth = options.outboundAuth === 'awsIam' ? { awsIam: {} } : { none: {} };
} else {
if (!options.providerArn) {
return { success: false, error: '--provider-arn is required when --outbound-auth oauth' };
}
if (!ARN_PATTERN.test(options.providerArn)) {
return { success: false, error: `Invalid --provider-arn '${options.providerArn}': must be a valid ARN` };
}
if (!options.scopes) {
return { success: false, error: '--scopes is required when --outbound-auth oauth' };
}
const scopes = options.scopes
.split(',')
.map(s => s.trim())
.filter(Boolean);
if (scopes.length === 0) {
return { success: false, error: '--scopes must contain at least one scope' };
}
if (
options.grantType !== undefined &&
!VALID_GRANT_TYPES.includes(options.grantType as (typeof VALID_GRANT_TYPES)[number])
) {
return {
success: false,
error: `Invalid --grant-type '${options.grantType}'. Valid: ${VALID_GRANT_TYPES.join(', ')}`,
};
}
outboundAuth = {
oauth: {
providerArn: options.providerArn,
scopes,
...(options.grantType && { grantType: options.grantType as (typeof VALID_GRANT_TYPES)[number] }),
},
};
}
}

const configIO = new ConfigIO();

// Resolve --gateway (project name) to ARN from deployed-state
Expand Down Expand Up @@ -98,7 +161,12 @@ export async function handleAddTool(options: AddToolOptions): Promise<AddToolRes
} else if (toolType === 'agentcore_code_interpreter' && options.codeInterpreterArn) {
toolEntry.config = { agentCoreCodeInterpreter: { codeInterpreterArn: options.codeInterpreterArn } };
} else if (toolType === 'agentcore_gateway') {
toolEntry.config = { agentCoreGateway: { gatewayArn: resolvedGatewayArn! } };
toolEntry.config = {
agentCoreGateway: {
gatewayArn: resolvedGatewayArn!,
...(outboundAuth && { outboundAuth }),
},
};
}

harnessSpec.tools.push(toolEntry);
Expand Down
17 changes: 17 additions & 0 deletions src/cli/commands/add/tool-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ export function registerAddTool(addCmd: Command): void {
.option('--code-interpreter-arn <arn>', 'Custom code interpreter ARN (optional for agentcore_code_interpreter)')
.option('--gateway-arn <arn>', 'Gateway ARN (for agentcore_gateway)')
.option('--gateway <name>', 'Project gateway name — resolves ARN from deployed state (for agentcore_gateway)')
.option(
'--outbound-auth <type>',
'Gateway outbound auth: awsIam, none, or oauth (default: awsIam if omitted) [agentcore_gateway]'
)
.option('--provider-arn <arn>', 'OAuth credential provider ARN (required when --outbound-auth oauth)')
.option(
'--scopes <scopes>',
'Comma-separated OAuth scopes (required when --outbound-auth oauth), e.g. "openid,profile" or "https://api.example.com/read"'
)
.option(
'--grant-type <type>',
'OAuth grant type: CLIENT_CREDENTIALS or USER_FEDERATION (for --outbound-auth oauth)'
)
.option('--json', 'Output as JSON')
.action(async cliOptions => {
if (!findConfigRoot()) {
Expand All @@ -35,6 +48,10 @@ export function registerAddTool(addCmd: Command): void {
codeInterpreterArn: cliOptions.codeInterpreterArn,
gatewayArn: cliOptions.gatewayArn,
gateway: cliOptions.gateway,
outboundAuth: cliOptions.outboundAuth,
providerArn: cliOptions.providerArn,
scopes: cliOptions.scopes,
grantType: cliOptions.grantType,
json: cliOptions.json,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,146 @@ describe('mapHarnessSpecToCreateOptions', () => {

expect(result.tools).toBeUndefined();
});

it('passes gateway tool with outboundAuth awsIam through the mapper', async () => {
const spec = minimalSpec({
tools: [
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: { awsIam: {} },
},
},
},
],
});

const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });

expect(result.tools).toEqual([
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: { awsIam: {} },
},
},
},
]);
});

it('passes gateway tool with outboundAuth none through the mapper', async () => {
const spec = minimalSpec({
tools: [
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: { none: {} },
},
},
},
],
});

const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });

expect(result.tools).toEqual([
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: { none: {} },
},
},
},
]);
});

it('passes gateway tool with outboundAuth oauth through the mapper', async () => {
const spec = minimalSpec({
tools: [
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: {
oauth: {
providerArn:
'arn:aws:bedrock-agentcore:us-west-2:123:token-vault/default/oauth2credentialprovider/my-provider',
scopes: ['read', 'write'],
grantType: 'CLIENT_CREDENTIALS',
},
},
},
},
},
],
});

const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });

expect(result.tools).toEqual([
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
outboundAuth: {
oauth: {
providerArn:
'arn:aws:bedrock-agentcore:us-west-2:123:token-vault/default/oauth2credentialprovider/my-provider',
scopes: ['read', 'write'],
grantType: 'CLIENT_CREDENTIALS',
},
},
},
},
},
]);
});

it('passes gateway tool without outboundAuth through the mapper', async () => {
const spec = minimalSpec({
tools: [
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
},
},
},
],
});

const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });

expect(result.tools).toEqual([
{
type: 'agentcore_gateway',
name: 'my_gw',
config: {
agentCoreGateway: {
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
},
},
},
]);
});
});

// ── Skills mapping ─────────────────────────────────────────────────────
Expand Down
29 changes: 28 additions & 1 deletion src/cli/primitives/HarnessPrimitive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APP_DIR, ConfigIO, findConfigRoot } from '../../lib';
import type {
HarnessGatewayOutboundAuth,
HarnessModelProvider,
HarnessSpec,
MemoryStrategy,
Expand Down Expand Up @@ -45,6 +46,9 @@ export interface AddHarnessOptions {
mcpName?: string;
mcpUrl?: string;
gatewayArn?: string;
gatewayOutboundAuth?: 'awsIam' | 'none' | 'oauth';
gatewayProviderArn?: string;
gatewayScopes?: string[];
authorizerType?: RuntimeAuthorizerType;
jwtConfig?: JwtConfigOptions;
configBaseDir?: string;
Expand Down Expand Up @@ -104,10 +108,33 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
config: { remoteMcp: { url: options.mcpUrl } },
});
} else if (toolType === 'agentcore_gateway' && options.gatewayArn) {
let outboundAuth: HarnessGatewayOutboundAuth | undefined;
if (options.gatewayOutboundAuth === 'awsIam') {
outboundAuth = { awsIam: {} };
} else if (options.gatewayOutboundAuth === 'none') {
outboundAuth = { none: {} };
} else if (
options.gatewayOutboundAuth === 'oauth' &&
options.gatewayProviderArn &&
options.gatewayScopes &&
options.gatewayScopes.length > 0
) {
outboundAuth = {
oauth: {
providerArn: options.gatewayProviderArn,
scopes: options.gatewayScopes,
},
};
}
Comment thread
notgitika marked this conversation as resolved.
tools.push({
type: 'agentcore_gateway',
name: 'gateway',
config: { agentCoreGateway: { gatewayArn: options.gatewayArn } },
config: {
agentCoreGateway: {
gatewayArn: options.gatewayArn,
...(outboundAuth && { outboundAuth }),
},
},
});
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/cli/tui/screens/harness/AddHarnessFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export function AddHarnessFlow({ isInteractive = true, onExit, onBack, onDev, on
mcpName: config.mcpName,
mcpUrl: config.mcpUrl,
gatewayArn: config.gatewayArn,
gatewayOutboundAuth: config.gatewayOutboundAuth,
gatewayProviderArn: config.gatewayProviderArn,
gatewayScopes: config.gatewayScopes
? config.gatewayScopes
.split(',')
.map(s => s.trim())
.filter(Boolean)
: undefined,
authorizerType: config.authorizerType,
jwtConfig: config.jwtConfig
? {
Expand Down
Loading
Loading