Skip to content

Commit f818286

Browse files
authored
feat: replace credentialProviderName with outboundAuth for harness gateway tools (#1083)
* feat: replace credentialProviderName with outboundAuth for harness gateway tools The CLI's harness schema for agentcore_gateway tools had a credentialProviderName field that didn't exist in the harness service model. The service expects an outboundAuth union with three members: awsIam (SigV4), none, and oauth (Bearer token via AgentCore Identity). - Replace credentialProviderName with outboundAuth union in harness schema (awsIam | none | oauth with providerArn, scopes, grantType, customParameters) - Add superRefine to reject legacy credentialProviderName with migration message - Add --outbound-auth, --provider-arn, --scopes, --grant-type flags to `agentcore add tool` command - Add outbound auth type selection to TUI harness wizard (3 new steps) - Update HarnessPrimitive to build outboundAuth config from options - Add schema tests for all outboundAuth variants and mapper round-trip tests * fix: reject oauth-only flags when outbound auth is awsIam or none When --outbound-auth is set to awsIam or none, the CLI now rejects --provider-arn, --scopes, and --grant-type instead of silently ignoring them. * fix: improve outbound auth DX — default indication, scopes example - CLI help: indicate awsIam is the default when --outbound-auth is omitted, add scope format examples to --scopes description - TUI: mark AWS IAM option as "(default)" in gateway auth selector
1 parent b873a49 commit f818286

12 files changed

Lines changed: 499 additions & 14 deletions

File tree

src/cli/commands/add/tool-action.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConfigIO } from '../../../lib';
2-
import type { HarnessSpec } from '../../../schema';
2+
import type { HarnessGatewayOutboundAuth, HarnessSpec } from '../../../schema';
33
import type { HarnessToolType } from '../../../schema/schemas/primitives/harness';
44

55
export interface AddToolOptions {
@@ -11,9 +11,17 @@ export interface AddToolOptions {
1111
codeInterpreterArn?: string;
1212
gatewayArn?: string;
1313
gateway?: string;
14+
outboundAuth?: string;
15+
providerArn?: string;
16+
scopes?: string;
17+
grantType?: string;
1418
json?: boolean;
1519
}
1620

21+
const VALID_OUTBOUND_AUTH_TYPES = ['awsIam', 'none', 'oauth'] as const;
22+
const VALID_GRANT_TYPES = ['CLIENT_CREDENTIALS', 'USER_FEDERATION'] as const;
23+
const ARN_PATTERN = /^arn:[^:]+:/;
24+
1725
export interface AddToolResult {
1826
success: boolean;
1927
error?: string;
@@ -49,6 +57,61 @@ export async function handleAddTool(options: AddToolOptions): Promise<AddToolRes
4957
return { success: false, error: '--gateway-arn or --gateway is required for agentcore_gateway tools' };
5058
}
5159

60+
let outboundAuth: HarnessGatewayOutboundAuth | undefined;
61+
if (options.outboundAuth !== undefined) {
62+
if (toolType !== 'agentcore_gateway') {
63+
return { success: false, error: '--outbound-auth is only valid for agentcore_gateway tools' };
64+
}
65+
if (!VALID_OUTBOUND_AUTH_TYPES.includes(options.outboundAuth as (typeof VALID_OUTBOUND_AUTH_TYPES)[number])) {
66+
return {
67+
success: false,
68+
error: `Invalid --outbound-auth '${options.outboundAuth}'. Valid: ${VALID_OUTBOUND_AUTH_TYPES.join(', ')}`,
69+
};
70+
}
71+
if (options.outboundAuth === 'awsIam' || options.outboundAuth === 'none') {
72+
if (options.providerArn || options.scopes || options.grantType) {
73+
return {
74+
success: false,
75+
error: '--provider-arn, --scopes, and --grant-type are only valid with --outbound-auth oauth',
76+
};
77+
}
78+
outboundAuth = options.outboundAuth === 'awsIam' ? { awsIam: {} } : { none: {} };
79+
} else {
80+
if (!options.providerArn) {
81+
return { success: false, error: '--provider-arn is required when --outbound-auth oauth' };
82+
}
83+
if (!ARN_PATTERN.test(options.providerArn)) {
84+
return { success: false, error: `Invalid --provider-arn '${options.providerArn}': must be a valid ARN` };
85+
}
86+
if (!options.scopes) {
87+
return { success: false, error: '--scopes is required when --outbound-auth oauth' };
88+
}
89+
const scopes = options.scopes
90+
.split(',')
91+
.map(s => s.trim())
92+
.filter(Boolean);
93+
if (scopes.length === 0) {
94+
return { success: false, error: '--scopes must contain at least one scope' };
95+
}
96+
if (
97+
options.grantType !== undefined &&
98+
!VALID_GRANT_TYPES.includes(options.grantType as (typeof VALID_GRANT_TYPES)[number])
99+
) {
100+
return {
101+
success: false,
102+
error: `Invalid --grant-type '${options.grantType}'. Valid: ${VALID_GRANT_TYPES.join(', ')}`,
103+
};
104+
}
105+
outboundAuth = {
106+
oauth: {
107+
providerArn: options.providerArn,
108+
scopes,
109+
...(options.grantType && { grantType: options.grantType as (typeof VALID_GRANT_TYPES)[number] }),
110+
},
111+
};
112+
}
113+
}
114+
52115
const configIO = new ConfigIO();
53116

54117
// Resolve --gateway (project name) to ARN from deployed-state
@@ -98,7 +161,12 @@ export async function handleAddTool(options: AddToolOptions): Promise<AddToolRes
98161
} else if (toolType === 'agentcore_code_interpreter' && options.codeInterpreterArn) {
99162
toolEntry.config = { agentCoreCodeInterpreter: { codeInterpreterArn: options.codeInterpreterArn } };
100163
} else if (toolType === 'agentcore_gateway') {
101-
toolEntry.config = { agentCoreGateway: { gatewayArn: resolvedGatewayArn! } };
164+
toolEntry.config = {
165+
agentCoreGateway: {
166+
gatewayArn: resolvedGatewayArn!,
167+
...(outboundAuth && { outboundAuth }),
168+
},
169+
};
102170
}
103171

104172
harnessSpec.tools.push(toolEntry);

src/cli/commands/add/tool-command.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ export function registerAddTool(addCmd: Command): void {
1818
.option('--code-interpreter-arn <arn>', 'Custom code interpreter ARN (optional for agentcore_code_interpreter)')
1919
.option('--gateway-arn <arn>', 'Gateway ARN (for agentcore_gateway)')
2020
.option('--gateway <name>', 'Project gateway name — resolves ARN from deployed state (for agentcore_gateway)')
21+
.option(
22+
'--outbound-auth <type>',
23+
'Gateway outbound auth: awsIam, none, or oauth (default: awsIam if omitted) [agentcore_gateway]'
24+
)
25+
.option('--provider-arn <arn>', 'OAuth credential provider ARN (required when --outbound-auth oauth)')
26+
.option(
27+
'--scopes <scopes>',
28+
'Comma-separated OAuth scopes (required when --outbound-auth oauth), e.g. "openid,profile" or "https://api.example.com/read"'
29+
)
30+
.option(
31+
'--grant-type <type>',
32+
'OAuth grant type: CLIENT_CREDENTIALS or USER_FEDERATION (for --outbound-auth oauth)'
33+
)
2134
.option('--json', 'Output as JSON')
2235
.action(async cliOptions => {
2336
if (!findConfigRoot()) {
@@ -35,6 +48,10 @@ export function registerAddTool(addCmd: Command): void {
3548
codeInterpreterArn: cliOptions.codeInterpreterArn,
3649
gatewayArn: cliOptions.gatewayArn,
3750
gateway: cliOptions.gateway,
51+
outboundAuth: cliOptions.outboundAuth,
52+
providerArn: cliOptions.providerArn,
53+
scopes: cliOptions.scopes,
54+
grantType: cliOptions.grantType,
3855
json: cliOptions.json,
3956
});
4057

src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,146 @@ describe('mapHarnessSpecToCreateOptions', () => {
199199

200200
expect(result.tools).toBeUndefined();
201201
});
202+
203+
it('passes gateway tool with outboundAuth awsIam through the mapper', async () => {
204+
const spec = minimalSpec({
205+
tools: [
206+
{
207+
type: 'agentcore_gateway',
208+
name: 'my_gw',
209+
config: {
210+
agentCoreGateway: {
211+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
212+
outboundAuth: { awsIam: {} },
213+
},
214+
},
215+
},
216+
],
217+
});
218+
219+
const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });
220+
221+
expect(result.tools).toEqual([
222+
{
223+
type: 'agentcore_gateway',
224+
name: 'my_gw',
225+
config: {
226+
agentCoreGateway: {
227+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
228+
outboundAuth: { awsIam: {} },
229+
},
230+
},
231+
},
232+
]);
233+
});
234+
235+
it('passes gateway tool with outboundAuth none through the mapper', async () => {
236+
const spec = minimalSpec({
237+
tools: [
238+
{
239+
type: 'agentcore_gateway',
240+
name: 'my_gw',
241+
config: {
242+
agentCoreGateway: {
243+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
244+
outboundAuth: { none: {} },
245+
},
246+
},
247+
},
248+
],
249+
});
250+
251+
const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });
252+
253+
expect(result.tools).toEqual([
254+
{
255+
type: 'agentcore_gateway',
256+
name: 'my_gw',
257+
config: {
258+
agentCoreGateway: {
259+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
260+
outboundAuth: { none: {} },
261+
},
262+
},
263+
},
264+
]);
265+
});
266+
267+
it('passes gateway tool with outboundAuth oauth through the mapper', async () => {
268+
const spec = minimalSpec({
269+
tools: [
270+
{
271+
type: 'agentcore_gateway',
272+
name: 'my_gw',
273+
config: {
274+
agentCoreGateway: {
275+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
276+
outboundAuth: {
277+
oauth: {
278+
providerArn:
279+
'arn:aws:bedrock-agentcore:us-west-2:123:token-vault/default/oauth2credentialprovider/my-provider',
280+
scopes: ['read', 'write'],
281+
grantType: 'CLIENT_CREDENTIALS',
282+
},
283+
},
284+
},
285+
},
286+
},
287+
],
288+
});
289+
290+
const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });
291+
292+
expect(result.tools).toEqual([
293+
{
294+
type: 'agentcore_gateway',
295+
name: 'my_gw',
296+
config: {
297+
agentCoreGateway: {
298+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
299+
outboundAuth: {
300+
oauth: {
301+
providerArn:
302+
'arn:aws:bedrock-agentcore:us-west-2:123:token-vault/default/oauth2credentialprovider/my-provider',
303+
scopes: ['read', 'write'],
304+
grantType: 'CLIENT_CREDENTIALS',
305+
},
306+
},
307+
},
308+
},
309+
},
310+
]);
311+
});
312+
313+
it('passes gateway tool without outboundAuth through the mapper', async () => {
314+
const spec = minimalSpec({
315+
tools: [
316+
{
317+
type: 'agentcore_gateway',
318+
name: 'my_gw',
319+
config: {
320+
agentCoreGateway: {
321+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
322+
},
323+
},
324+
},
325+
],
326+
});
327+
328+
const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec });
329+
330+
expect(result.tools).toEqual([
331+
{
332+
type: 'agentcore_gateway',
333+
name: 'my_gw',
334+
config: {
335+
agentCoreGateway: {
336+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123:gateway/abc',
337+
},
338+
},
339+
},
340+
]);
341+
});
202342
});
203343

204344
// ── Skills mapping ─────────────────────────────────────────────────────

src/cli/primitives/HarnessPrimitive.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { APP_DIR, ConfigIO, findConfigRoot } from '../../lib';
22
import type {
3+
HarnessGatewayOutboundAuth,
34
HarnessModelProvider,
45
HarnessSpec,
56
MemoryStrategy,
@@ -45,6 +46,9 @@ export interface AddHarnessOptions {
4546
mcpName?: string;
4647
mcpUrl?: string;
4748
gatewayArn?: string;
49+
gatewayOutboundAuth?: 'awsIam' | 'none' | 'oauth';
50+
gatewayProviderArn?: string;
51+
gatewayScopes?: string[];
4852
authorizerType?: RuntimeAuthorizerType;
4953
jwtConfig?: JwtConfigOptions;
5054
configBaseDir?: string;
@@ -104,10 +108,33 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
104108
config: { remoteMcp: { url: options.mcpUrl } },
105109
});
106110
} else if (toolType === 'agentcore_gateway' && options.gatewayArn) {
111+
let outboundAuth: HarnessGatewayOutboundAuth | undefined;
112+
if (options.gatewayOutboundAuth === 'awsIam') {
113+
outboundAuth = { awsIam: {} };
114+
} else if (options.gatewayOutboundAuth === 'none') {
115+
outboundAuth = { none: {} };
116+
} else if (
117+
options.gatewayOutboundAuth === 'oauth' &&
118+
options.gatewayProviderArn &&
119+
options.gatewayScopes &&
120+
options.gatewayScopes.length > 0
121+
) {
122+
outboundAuth = {
123+
oauth: {
124+
providerArn: options.gatewayProviderArn,
125+
scopes: options.gatewayScopes,
126+
},
127+
};
128+
}
107129
tools.push({
108130
type: 'agentcore_gateway',
109131
name: 'gateway',
110-
config: { agentCoreGateway: { gatewayArn: options.gatewayArn } },
132+
config: {
133+
agentCoreGateway: {
134+
gatewayArn: options.gatewayArn,
135+
...(outboundAuth && { outboundAuth }),
136+
},
137+
},
111138
});
112139
}
113140
}

src/cli/tui/screens/harness/AddHarnessFlow.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export function AddHarnessFlow({ isInteractive = true, onExit, onBack, onDev, on
6868
mcpName: config.mcpName,
6969
mcpUrl: config.mcpUrl,
7070
gatewayArn: config.gatewayArn,
71+
gatewayOutboundAuth: config.gatewayOutboundAuth,
72+
gatewayProviderArn: config.gatewayProviderArn,
73+
gatewayScopes: config.gatewayScopes
74+
? config.gatewayScopes
75+
.split(',')
76+
.map(s => s.trim())
77+
.filter(Boolean)
78+
: undefined,
7179
authorizerType: config.authorizerType,
7280
jwtConfig: config.jwtConfig
7381
? {

0 commit comments

Comments
 (0)