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
35 changes: 22 additions & 13 deletions src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,10 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep

// Unified .env.local existence check across ApiKey, OAuth2, and Payment credentials.
// Lists every required env var upfront so the user can populate the file in one shot.
const envFileError = assertEnvFileExists(context.projectSpec, configIO.getConfigRoot());
if (envFileError) {
const envFileAssertionResult = assertEnvFileExists(context.projectSpec, configIO.getConfigRoot());
if (!envFileAssertionResult.success) {
logger.finalize(false);
return { success: false, error: new Error(envFileError), logPath: logger.getRelativeLogPath() };
return { success: false, error: envFileAssertionResult.error, logPath: logger.getRelativeLogPath() };
}

// Read runtime credentials from process.env (enables non-interactive deploy with -y)
Expand Down Expand Up @@ -270,12 +270,16 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
enableKmsEncryption: true,
});
if (identityResult.hasErrors) {
const errorResult = identityResult.results.find(r => r.status === 'error');
const errorMsg =
errorResult?.error && typeof errorResult.error === 'string' ? errorResult.error : 'Identity setup failed';
const errorResult = identityResult.results.find(r => r.status === 'error' && r.error);
const errorMsg = errorResult?.error?.message ?? 'Identity setup failed';
endStep('error', errorMsg);
logger.finalize(false);
return { success: false, error: new Error(errorMsg), logPath: logger.getRelativeLogPath() };

return {
success: false,
error: errorResult?.error ?? new Error('unknown error occurred when setting up api key providers'),
logPath: logger.getRelativeLogPath(),
};
}
identityKmsKeyArn = identityResult.kmsKeyArn;

Expand All @@ -302,12 +306,17 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
});
if (oauthResult.hasErrors) {
// Log detailed error internally, return sanitized message to avoid leaking OAuth details
const errorResult = oauthResult.results.find(r => r.status === 'error');
const errorResult = oauthResult.results.find(r => r.status === 'error' && r.error);
logger.log(`OAuth setup error: ${errorResult?.error ?? 'unknown'}`, 'error');
const errorMsg = 'OAuth credential setup failed. Check the log for details.';
endStep('error', errorMsg);
logger.finalize(false);
return { success: false, error: new Error(errorMsg), logPath: logger.getRelativeLogPath() };

return {
success: false,
error: errorResult?.error ?? new Error(`an unexpected error ocurred when setting up oauth providers`),
logPath: logger.getRelativeLogPath(),
Comment thread
Hweinstock marked this conversation as resolved.
};
}

// Collect OAuth credential ARNs for deployed state
Expand Down Expand Up @@ -336,13 +345,13 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
});

if (paymentPreDeployResult.hasErrors) {
const errorMsgs = paymentPreDeployResult.errors.join('; ');
const errorMsgs = paymentPreDeployResult.errors.map(e => e.message).join('; ');
endStep('error', errorMsgs);
logger.log(`Payment credential setup errors: ${errorMsgs}`, 'error');
logger.finalize(false);
return {
success: false,
error: new Error(`Payment setup failed: ${errorMsgs}`),
error: paymentPreDeployResult.errors[0] ?? new Error('payment deploy preflight steps failed'),
logPath: logger.getRelativeLogPath(),
};
}
Expand Down Expand Up @@ -405,7 +414,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
logger.finalize(false);
return {
success: false,
error: new Error('AWS environment needs bootstrapping. Run with --yes to auto-bootstrap.'),
error: new ValidationError('AWS environment needs bootstrapping. Run with --yes to auto-bootstrap.'),
logPath: logger.getRelativeLogPath(),
};
}
Expand Down Expand Up @@ -517,7 +526,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
logger.finalize(false);
return {
success: false,
error: new Error(`Stack teardown failed: ${teardownError}`),
error: teardown.error,
logPath: logger.getRelativeLogPath(),
};
}
Expand Down
12 changes: 6 additions & 6 deletions src/cli/commands/dev/command.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ConnectionError,
DevServerConnectionError,
NoProjectError,
ResourceNotFoundError,
ValidationError,
Expand Down Expand Up @@ -65,7 +65,7 @@ async function invokeDevServer(
}
} catch (err) {
throw isConnectionRefused(err)
? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
cause: err,
})
: err;
Expand All @@ -80,7 +80,7 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record
process.stdout.write('\n');
} catch (err) {
throw isConnectionRefused(err)
? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
cause: err,
})
: err;
Expand All @@ -89,8 +89,8 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record

function isConnectionRefused(err: unknown): boolean {
if (!(err instanceof Error)) return false;
// ConnectionError from invoke.ts wraps fetch failures after retries
if (err.name === 'ConnectionError') return true;
// DevServerConnectionError from invoke.ts wraps fetch failures after retries
if (err.name === 'DevServerConnectionError') return true;
const msg = err.message + (err.cause instanceof Error ? err.cause.message : '');
return msg.includes('ECONNREFUSED') || msg.includes('fetch failed');
}
Expand Down Expand Up @@ -138,7 +138,7 @@ async function handleMcpInvoke(
}
} catch (err) {
throw isConnectionRefused(err)
? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, {
cause: err,
})
: err;
Expand Down
51 changes: 33 additions & 18 deletions src/cli/operations/deploy/__tests__/assert-env-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ describe('assertEnvFileExists', () => {
vi.clearAllMocks();
});

it('returns null when no credentials exist (file missing is fine)', () => {
it('returns ok when no credentials exist (file missing is fine)', () => {
mockExistsSync.mockReturnValue(false);
const result = assertEnvFileExists(makeSpec(), BASE_DIR);
expect(result).toBeNull();
expect(result.success).toBe(true);
});

it('returns null when file exists', () => {
it('returns ok when file exists', () => {
mockExistsSync.mockReturnValue(true);
const spec = makeSpec({
credentials: [{ name: 'mykey', authorizerType: 'ApiKeyCredentialProvider' } as any],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toBeNull();
expect(result.success).toBe(true);
});

it('lists ApiKey env vars when file is missing', () => {
Expand All @@ -57,8 +57,11 @@ describe('assertEnvFileExists', () => {
credentials: [{ name: 'openai', authorizerType: 'ApiKeyCredentialProvider' } as any],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toContain('agentcore/.env.local not found');
expect(result).toContain('AGENTCORE_CREDENTIAL_OPENAI');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toContain('agentcore/.env.local not found');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_OPENAI');
}
});

it('lists OAuth2 env vars when file is missing', () => {
Expand All @@ -67,8 +70,11 @@ describe('assertEnvFileExists', () => {
credentials: [{ name: 'google-oauth', authorizerType: 'OAuthCredentialProvider' } as any],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_ID');
expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_SECRET');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_ID');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_SECRET');
}
});

it('lists CoinbaseCDP payment env vars when file is missing', () => {
Expand All @@ -83,9 +89,12 @@ describe('assertEnvFileExists', () => {
],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID');
expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_SECRET');
expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_WALLET_SECRET');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_SECRET');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_WALLET_SECRET');
}
});

it('lists StripePrivy payment env vars when file is missing', () => {
Expand All @@ -102,10 +111,13 @@ describe('assertEnvFileExists', () => {
],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toContain('APP_ID');
expect(result).toContain('APP_SECRET');
expect(result).toContain('AUTHORIZATION_PRIVATE_KEY');
expect(result).toContain('AUTHORIZATION_ID');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toContain('APP_ID');
expect(result.error.message).toContain('APP_SECRET');
expect(result.error.message).toContain('AUTHORIZATION_PRIVATE_KEY');
expect(result.error.message).toContain('AUTHORIZATION_ID');
}
});

it('combines all credential types in a single error', () => {
Expand All @@ -124,9 +136,12 @@ describe('assertEnvFileExists', () => {
],
});
const result = assertEnvFileExists(spec, BASE_DIR);
expect(result).toContain('AGENTCORE_CREDENTIAL_OPENAI');
expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_CLIENT_ID');
expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_OPENAI');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_CLIENT_ID');
expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID');
}
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ vi.mock('../../../../lib/index.js', () => ({
}
},
readEnvFile: mockReadEnvFile,
MissingCredentialsError: class MissingCredentialsError extends Error {
constructor(message: string) {
super(message);
this.name = 'MissingCredentialsError';
}
},
toError: (e: unknown) => (e instanceof Error ? e : new Error(String(e))),
}));

vi.mock('../../../aws/index.js', () => ({
Expand Down Expand Up @@ -359,7 +366,7 @@ describe('setupOAuth2Providers', () => {
expect(result.hasErrors).toBe(false);
expect(result.results).toHaveLength(1);
expect(result.results[0]!.status).toBe('skipped');
expect(result.results[0]!.error).toContain('Missing');
expect(result.results[0]!.error?.message).toContain('Missing');
});

it('returns error on failure', async () => {
Expand Down
21 changes: 14 additions & 7 deletions src/cli/operations/deploy/__tests__/pre-deploy-payments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ vi.mock('../../../../lib', () => ({
}
},
readEnvFile: mockReadEnvFile,
toError: (e: unknown) => (e instanceof Error ? e : new Error(String(e))),
MissingCredentialsError: class MissingCredentialsError extends Error {
constructor(message: string) {
super(message);
this.name = 'MissingCredentialsError';
}
},
}));

vi.mock('fs', () => ({
Expand Down Expand Up @@ -218,9 +225,9 @@ describe('setupPaymentCredentialProviders', () => {

expect(result.hasErrors).toBe(true);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('Missing CDP credentials');
expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_API_KEY_SECRET');
expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_WALLET_SECRET');
expect(result.errors[0]?.message).toContain('Missing CDP credentials');
expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_API_KEY_SECRET');
expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_WALLET_SECRET');
expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled();
});

Expand All @@ -240,10 +247,10 @@ describe('setupPaymentCredentialProviders', () => {

expect(result.hasErrors).toBe(true);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('Missing StripePrivy credentials');
expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_APP_SECRET');
expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_PRIVATE_KEY');
expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_ID');
expect(result.errors[0]?.message).toContain('Missing StripePrivy credentials');
expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_APP_SECRET');
expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_PRIVATE_KEY');
expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_ID');
expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled();
});

Expand Down
Loading
Loading