Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fetchRuntimeAccess } from '../fetch-runtime-access';
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../../../lib', () => ({
ConfigIO: vi.fn(),
}));

const RUNTIME_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my-agent-abc123';
const EXPECTED_URL = `https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/${encodeURIComponent(RUNTIME_ARN)}/invocations`;

const deployedState = {
targets: {
default: {
resources: {
runtimes: {
'my-agent': { runtimeId: 'rt-1', runtimeArn: RUNTIME_ARN, roleArn: 'arn:aws:iam::123:role/r' },
},
},
},
},
};

const projectSpec = {
runtimes: [{ name: 'my-agent', authorizerType: 'AWS_IAM' }],
credentials: [],
};

const awsTargets = [{ name: 'default', region: 'us-east-1', account: '123456789012' }];

function createMockConfigIO() {
return {
readDeployedState: vi.fn().mockResolvedValue(deployedState),
readProjectSpec: vi.fn().mockResolvedValue(projectSpec),
readAWSDeploymentTargets: vi.fn().mockResolvedValue(awsTargets),
} as any;
}

describe('fetchRuntimeAccess', () => {
afterEach(() => vi.clearAllMocks());

it('returns the runtime invocation URL and SigV4 message for an AWS_IAM agent, with no token', async () => {
const result = await fetchRuntimeAccess('my-agent', { configIO: createMockConfigIO() });

expect(result.url).toBe(EXPECTED_URL);
expect(result.authType).toBe('AWS_IAM');
expect(result.token).toBeUndefined();
expect(result.message).toContain('SigV4');
});
});
78 changes: 78 additions & 0 deletions src/cli/operations/fetch-access/fetch-runtime-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ConfigIO } from '../../../lib';
import { buildRuntimeInvocationUrl } from '../../commands/status/constants';
import { fetchOAuthToken } from './oauth-token';
import type { TokenFetchResult } from './types';

/**
* Resolve invoke access for a deployed agent runtime.
*
* AWS_IAM agents have no token to fetch (SigV4 signing is used instead) but DO have an
* invoke URL, so we surface the runtime invocation URL plus a SigV4 message — parity with
* the AWS_IAM gateway path. CUSTOM_JWT agents additionally fetch an OAuth access token.
*/
export async function fetchRuntimeAccess(
agentName: string,
options: { configIO?: ConfigIO; deployTarget?: string; identityName?: string } = {}
): Promise<TokenFetchResult> {
const configIO = options.configIO ?? new ConfigIO();

const deployedState = await configIO.readDeployedState();
const projectSpec = await configIO.readProjectSpec();
const awsTargets = await configIO.readAWSDeploymentTargets();

const targetNames = Object.keys(deployedState.targets);
if (targetNames.length === 0) {
throw new Error('No deployed targets found. Run `agentcore deploy` first.');
}

const targetName = options.deployTarget ?? targetNames[0]!;
const target = deployedState.targets[targetName];
if (!target) {
throw new Error(`Deployment target '${targetName}' not found. Available targets: ${targetNames.join(', ')}`);
}

const agentSpec = projectSpec.runtimes.find(a => a.name === agentName);
if (!agentSpec) {
const available = projectSpec.runtimes.map(a => a.name);
throw new Error(`Agent '${agentName}' not found in project. Available agents: ${available.join(', ') || 'none'}`);
}

const deployedRuntime = target.resources?.runtimes?.[agentName];
if (!deployedRuntime?.runtimeArn) {
throw new Error(`Agent '${agentName}' does not have a deployed runtime. Run \`agentcore deploy\` first.`);
}

const region = awsTargets.find(t => t.name === targetName)?.region;
const url = region ? buildRuntimeInvocationUrl(region, deployedRuntime.runtimeArn) : '';

const authType = agentSpec.authorizerType ?? 'AWS_IAM';

if (authType === 'AWS_IAM') {
return {
url,
authType: 'AWS_IAM',
message: 'This agent uses AWS_IAM authentication. Use AWS SigV4 signing to invoke.',
};
}

const jwtConfig = agentSpec.authorizerConfiguration?.customJwtAuthorizer;
if (!jwtConfig) {
throw new Error(`Agent '${agentName}' is configured as CUSTOM_JWT but has no customJwtAuthorizer configuration.`);
}

const result = await fetchOAuthToken({
resourceName: agentName,
jwtConfig,
deployedState,
targetName,
credentials: projectSpec.credentials,
credentialName: options.identityName,
});

return {
url,
authType: 'CUSTOM_JWT',
token: result.token,
expiresIn: result.expiresIn,
};
}
1 change: 1 addition & 0 deletions src/cli/operations/fetch-access/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { fetchGatewayToken } from './fetch-gateway-token';
export { canFetchHarnessToken, fetchHarnessToken } from './fetch-harness-token';
export { fetchRuntimeAccess } from './fetch-runtime-access';
export { canFetchRuntimeToken, fetchRuntimeToken } from './fetch-runtime-token';
export { fetchOAuthToken } from './oauth-token';
export type { OAuthTokenResult } from './oauth-token';
Expand Down
8 changes: 4 additions & 4 deletions src/cli/tui/screens/fetch-access/useFetchAccessFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { OAuthTokenResult, ResourceInfo, TokenFetchResult } from '../../../
import {
fetchGatewayToken,
fetchHarnessToken,
fetchRuntimeToken,
fetchRuntimeAccess,
listAgents,
listGateways,
listHarnesses,
Expand All @@ -13,8 +13,8 @@ import { spawn } from 'node:child_process';
import { useCallback, useEffect, useRef, useState } from 'react';

/**
* Resolve token-bearing access for an agent runtime or harness. AWS_IAM resources have no token
* to fetch (SigV4 signing is used instead); CUSTOM_JWT resources fetch an OAuth token directly,
* Resolve token-bearing access for a harness. AWS_IAM harnesses have no token to fetch
* (SigV4 signing is used instead); CUSTOM_JWT harnesses fetch an OAuth token directly,
* with any error (missing credential, bad config) surfacing in the error phase.
*/
async function fetchTokenAccess(
Expand Down Expand Up @@ -127,7 +127,7 @@ export function useFetchAccessFlow() {
? fetchGatewayToken(resource.name)
: resource.resourceType === 'harness'
? fetchTokenAccess(resource, fetchHarnessToken)
: fetchTokenAccess(resource, fetchRuntimeToken);
: fetchRuntimeAccess(resource.name);

fetchToken
.then(result => {
Expand Down
Loading