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
8 changes: 7 additions & 1 deletion src/cli/cloudformation/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export function buildDeployedState(
agents: Record<string, AgentCoreDeployedState>,
gateways: Record<string, { gatewayId: string; gatewayArn: string }>,
existingState?: DeployedState,
identityKmsKeyArn?: string
identityKmsKeyArn?: string,
credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>
): DeployedState {
const targetState: TargetDeployedState = {
resources: {
Expand All @@ -192,6 +193,11 @@ export function buildDeployedState(
};
}

// Add credential state if credentials exist
if (credentials && Object.keys(credentials).length > 0) {
targetState.resources!.credentials = credentials;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is ! needed? we just checked for it above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! is required — resources is typed as optional in TargetDeployedState even though we initialize it in the literal above. TypeScript can't narrow it.

}

return {
targets: {
...existingState?.targets,
Expand Down
65 changes: 52 additions & 13 deletions src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
checkStackDeployability,
getAllCredentials,
hasOwnedIdentityApiProviders,
hasOwnedIdentityOAuthProviders,
performStackTeardown,
setupApiKeyProviders,
setupOAuth2Providers,
synthesizeCdk,
validateProject,
} from '../../operations/deploy';
Expand Down Expand Up @@ -166,20 +168,21 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep

// Set up identity providers if needed
let identityKmsKeyArn: string | undefined;
if (hasOwnedIdentityApiProviders(context.projectSpec)) {
startStep('Creating credentials...');

// In CLI mode, also check process.env for credentials (enables non-interactive deploy with -y)
const neededCredentials = getAllCredentials(context.projectSpec);
const envCredentials: Record<string, string> = {};
for (const cred of neededCredentials) {
const value = process.env[cred.envVarName];
if (value) {
envCredentials[cred.envVarName] = value;
}
// Read runtime credentials from process.env (enables non-interactive deploy with -y)
const neededCredentials = getAllCredentials(context.projectSpec);
const envCredentials: Record<string, string> = {};
for (const cred of neededCredentials) {
const value = process.env[cred.envVarName];
if (value) {
envCredentials[cred.envVarName] = value;
}
const runtimeCredentials =
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;
}
const runtimeCredentials =
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;

if (hasOwnedIdentityApiProviders(context.projectSpec)) {
startStep('Creating credentials...');

const identityResult = await setupApiKeyProviders({
projectSpec: context.projectSpec,
Expand All @@ -200,6 +203,41 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
endStep('success');
}

// Set up OAuth credential providers if needed
const oauthCredentials: Record<
string,
{ credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }
> = {};
if (hasOwnedIdentityOAuthProviders(context.projectSpec)) {
startStep('Creating OAuth credentials...');

const oauthResult = await setupOAuth2Providers({
projectSpec: context.projectSpec,
configBaseDir: configIO.getConfigRoot(),
region: target.region,
runtimeCredentials,
});
if (oauthResult.hasErrors) {
const errorResult = oauthResult.results.find(r => r.status === 'error');
const errorMsg = errorResult?.error ?? 'OAuth credential setup failed';
endStep('error', errorMsg);
logger.finalize(false);
return { success: false, error: errorMsg, logPath: logger.getRelativeLogPath() };
}

// Collect credential ARNs for deployed state
for (const result of oauthResult.results) {
if (result.credentialProviderArn) {
oauthCredentials[result.providerName] = {
credentialProviderArn: result.credentialProviderArn,
clientSecretArn: result.clientSecretArn,
callbackUrl: result.callbackUrl,
};
}
}
endStep('success');
}

// Deploy
const hasGateways = mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0;
const deployStepName = hasGateways ? 'Deploying gateways...' : 'Deploy to AWS';
Expand Down Expand Up @@ -273,7 +311,8 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
agents,
gateways,
existingState,
identityKmsKeyArn
identityKmsKeyArn,
oauthCredentials
);
await configIO.writeDeployedState(deployedState);

Expand Down
5 changes: 5 additions & 0 deletions src/cli/operations/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ export {
// Pre-deploy identity setup for non-Bedrock model providers
export {
setupApiKeyProviders,
setupOAuth2Providers,
hasOwnedIdentityApiProviders,
hasOwnedIdentityOAuthProviders,
getMissingCredentials,
getAllCredentials,
type SetupApiKeyProvidersOptions,
type SetupOAuth2ProvidersOptions,
type PreDeployIdentityResult,
type PreDeployOAuth2Result,
type ApiKeyProviderSetupResult,
type OAuth2ProviderSetupResult,
type MissingCredential,
} from './pre-deploy-identity';

Expand Down
140 changes: 139 additions & 1 deletion src/cli/operations/deploy/pre-deploy-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { getCredentialProvider } from '../../aws';
import { isNoCredentialsError } from '../../errors';
import { apiKeyProviderExists, createApiKeyProvider, setTokenVaultKmsKey, updateApiKeyProvider } from '../identity';
import { computeDefaultCredentialEnvVarName } from '../identity/create-identity';
import {
createOAuth2Provider,
oAuth2ProviderExists,
updateOAuth2Provider,
} from '../identity/oauth2-credential-provider';
import { BedrockAgentCoreControlClient, GetTokenVaultCommand } from '@aws-sdk/client-bedrock-agentcore-control';
import { CreateKeyCommand, KMSClient } from '@aws-sdk/client-kms';

Expand Down Expand Up @@ -222,7 +227,7 @@ export async function getMissingCredentials(
}

/**
* Get list of all API key credentials in the project (for manual entry prompt).
* Get list of all credentials in the project that need env vars (for manual entry prompt and runtime credential reading).
*/
export function getAllCredentials(projectSpec: AgentCoreProjectSpec): MissingCredential[] {
const credentials: MissingCredential[] = [];
Expand All @@ -233,8 +238,141 @@ export function getAllCredentials(projectSpec: AgentCoreProjectSpec): MissingCre
providerName: credential.name,
envVarName: computeDefaultCredentialEnvVarName(credential.name),
});
} else if (credential.type === 'OAuthCredentialProvider') {
const nameKey = credential.name.toUpperCase().replace(/-/g, '_');
credentials.push(
{ providerName: credential.name, envVarName: `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_ID` },
{ providerName: credential.name, envVarName: `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_SECRET` }
);
}
}

return credentials;
}

// ─────────────────────────────────────────────────────────────────────────────
// OAuth2 Credential Provider Setup
// ─────────────────────────────────────────────────────────────────────────────

export interface OAuth2ProviderSetupResult {
providerName: string;
status: 'created' | 'updated' | 'skipped' | 'error';
error?: string;
credentialProviderArn?: string;
clientSecretArn?: string;
callbackUrl?: string;
}

export interface SetupOAuth2ProvidersOptions {
projectSpec: AgentCoreProjectSpec;
configBaseDir: string;
region: string;
runtimeCredentials?: SecureCredentials;
}

export interface PreDeployOAuth2Result {
results: OAuth2ProviderSetupResult[];
hasErrors: boolean;
}

/**
* Set up OAuth2 credential providers for all OAuth credentials in the project.
* Reads client credentials from agentcore/.env.local and creates providers in AgentCore Identity.
*/
export async function setupOAuth2Providers(options: SetupOAuth2ProvidersOptions): Promise<PreDeployOAuth2Result> {
const { projectSpec, configBaseDir, region, runtimeCredentials } = options;
const results: OAuth2ProviderSetupResult[] = [];
const credentials = getCredentialProvider();

const envVars = await readEnvFile(configBaseDir);
const envCredentials = SecureCredentials.fromEnvVars(envVars);
const allCredentials = runtimeCredentials ? envCredentials.merge(runtimeCredentials) : envCredentials;

const client = new BedrockAgentCoreControlClient({ region, credentials });

for (const credential of projectSpec.credentials) {
if (credential.type === 'OAuthCredentialProvider') {
const result = await setupSingleOAuth2Provider(client, credential, allCredentials);
results.push(result);
}
}

return {
results,
hasErrors: results.some(r => r.status === 'error'),
};
}
Comment on lines +282 to +304
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client is created per-call to setupOAuth2Providers but not shared with setupApiKeyProviders.

Both functions create their own BedrockAgentCoreControlClient. If credentials or region config diverge between the two calls, you'd get inconsistent behavior.

This adds another duplicated client instance. We have an existing issue: #342

Maybe this can be addressed in a diff PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the duplicate client is a known issue tracked in #342. Happy to address here if you'd prefer, but keeping it consistent with the existing setupApiKeyProviders pattern for now.


/**
* Check if the project has any OAuth credentials that need setup.
*/
export function hasOwnedIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean {
return projectSpec.credentials.some(c => c.type === 'OAuthCredentialProvider');
}

async function setupSingleOAuth2Provider(
client: BedrockAgentCoreControlClient,
credential: Credential,
credentials: SecureCredentials
): Promise<OAuth2ProviderSetupResult> {
if (credential.type !== 'OAuthCredentialProvider') {
return { providerName: credential.name, status: 'error', error: 'Invalid credential type' };
}

const nameKey = credential.name.toUpperCase().replace(/-/g, '_');
const clientIdEnvVar = `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_ID`;
const clientSecretEnvVar = `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_SECRET`;

const clientId = credentials.get(clientIdEnvVar);
const clientSecret = credentials.get(clientSecretEnvVar);

if (!clientId || !clientSecret) {
return {
providerName: credential.name,
status: 'skipped',
error: `Missing ${clientIdEnvVar} or ${clientSecretEnvVar} in agentcore/.env.local`,
};
}

const params = {
name: credential.name,
vendor: credential.vendor,
discoveryUrl: credential.discoveryUrl,
clientId,
clientSecret,
};

try {
const exists = await oAuth2ProviderExists(client, credential.name);

if (exists) {
const updateResult = await updateOAuth2Provider(client, params);
return {
providerName: credential.name,
status: updateResult.success ? 'updated' : 'error',
error: updateResult.error,
credentialProviderArn: updateResult.result?.credentialProviderArn,
clientSecretArn: updateResult.result?.clientSecretArn,
callbackUrl: updateResult.result?.callbackUrl,
};
}

const createResult = await createOAuth2Provider(client, params);
return {
providerName: credential.name,
status: createResult.success ? 'created' : 'error',
error: createResult.error,
credentialProviderArn: createResult.result?.credentialProviderArn,
clientSecretArn: createResult.result?.clientSecretArn,
callbackUrl: createResult.result?.callbackUrl,
};
} catch (error) {
let errorMessage: string;
if (isNoCredentialsError(error)) {
errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.';
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
return { providerName: credential.name, status: 'error', error: errorMessage };
}
}
8 changes: 8 additions & 0 deletions src/cli/operations/identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ export {
setTokenVaultKmsKey,
updateApiKeyProvider,
} from './api-key-credential-provider';
export {
createOAuth2Provider,
getOAuth2Provider,
oAuth2ProviderExists,
updateOAuth2Provider,
type OAuth2ProviderParams,
type OAuth2ProviderResult,
} from './oauth2-credential-provider';
export {
computeDefaultCredentialEnvVarName,
resolveCredentialStrategy,
Expand Down
Loading
Loading