Skip to content

Commit 8c52e2f

Browse files
committed
add script to link OPA secrets to agent and validate OPA configuration
1 parent 472df87 commit 8c52e2f

16 files changed

+1753
-153
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"validate:okta": "tsx scripts/validate-okta-config.ts",
3333
"validate:opa": "tsx scripts/validate-opa-secrets.ts",
3434
"rollback:okta": "tsx scripts/rollback-okta-config.ts",
35-
"bootstrap:opa": "tsx scripts/setup-opa-secrets.ts",
35+
"setup:opa": "tsx scripts/setup-opa-secrets.ts",
36+
"link:opa": "tsx scripts/link-opa-secrets.ts",
3637
"typecheck:scripts": "tsc --project scripts/tsconfig.json --noEmit"
3738
},
3839
"keywords": [],

packages/agent0/src/agent.ts

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,24 @@ function validateAgentLLMEnv(): AgentLLMConfig {
145145

146146
// Error if neither provider is configured
147147
if (!hasAnthropicKey && !hasBedrockVars) {
148-
console.error('❌ Environment configuration error in .env.agent');
149-
console.error(' No LLM provider configured');
150-
console.error(' Please configure one LLM provider:');
151-
console.error(' - For Anthropic: Set ANTHROPIC_API_KEY and ANTHROPIC_MODEL');
152-
console.error(' - For Bedrock: Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, BEDROCK_MODEL_ID');
148+
console.error('❌ No LLM credentials configured\n');
149+
console.error(' You have two options to configure LLM credentials:\n');
150+
console.error(' Option 1: Direct Mode (simple, credentials in .env.agent)');
151+
console.error(' ─────────────────────────────────────────────────────────');
152+
console.error(' For Anthropic:');
153+
console.error(' ANTHROPIC_API_KEY=sk-ant-...');
154+
console.error(' ANTHROPIC_MODEL=claude-sonnet-4-20250514\n');
155+
console.error(' For AWS Bedrock:');
156+
console.error(' AWS_REGION=us-east-1');
157+
console.error(' AWS_ACCESS_KEY_ID=AKIA...');
158+
console.error(' AWS_SECRET_ACCESS_KEY=...');
159+
console.error(' BEDROCK_MODEL_ID=anthropic.claude-3-sonnet-20240229-v1:0\n');
160+
console.error(' Option 2: OPA Mode (secure, credentials from Okta PAM)');
161+
console.error(' ─────────────────────────────────────────────────────────');
162+
console.error(' Requires .env.opa with:');
163+
console.error(' OPA_LLM_PROVIDER=anthropic');
164+
console.error(' OPA_ANTHROPIC_API_KEY_ORN=orn:okta:pam:{orgId}:secrets:{secretId}\n');
165+
console.error(' Run: pnpm run setup:opa (then link secrets to agent)\n');
153166
process.exit(1);
154167
}
155168

@@ -263,23 +276,18 @@ export function isLLMConfigInitialized(): boolean {
263276
return llmConfigInitialized;
264277
}
265278

266-
// Try synchronous initialization at module load (for backwards compatibility)
267-
// This will only work if env vars are set and OPA is not configured
268-
// Try synchronous initialization from env vars at module load
269-
// OPA credentials are fetched per-user-session, so we don't init them here
270-
try {
271-
if (!isOPAConfigured()) {
272-
// No OPA configured, must use env vars
273-
llmConfig = validateAgentLLMEnv();
274-
llmConfigSource = 'env';
275-
llmConfigInitialized = true;
276-
}
277-
// If OPA is configured, credentials will be fetched in getAgentForUserContext()
278-
} catch {
279-
// If env var init fails and OPA is configured, that's fine - will use OPA per-session
280-
if (!isOPAConfigured()) {
281-
console.error('LLM configuration failed - no env vars configured and OPA not available');
282-
}
279+
// Initialize LLM configuration at module load
280+
// OPA credentials are fetched per-user-session, env vars are loaded at startup
281+
if (isOPAConfigured()) {
282+
// OPA mode: credentials will be fetched per-user via token exchange
283+
console.log('🔐 OPA mode enabled - LLM credentials will be fetched per-user session');
284+
llmConfigInitialized = true;
285+
llmConfigSource = 'opa';
286+
} else {
287+
// Direct mode: validate and load env vars now
288+
llmConfig = validateAgentLLMEnv();
289+
llmConfigSource = 'env';
290+
llmConfigInitialized = true;
283291
}
284292

285293
// ============================================================================
@@ -373,15 +381,12 @@ function buildAgentConfig(): Omit<AgentConfig, 'idToken' | 'userContext'> | null
373381
}
374382

375383
export async function getAgentForUserContext(idToken: string, userContext: UserContext): Promise<Agent> {
376-
// Try OPA token exchange for credential retrieval (per-user-session)
384+
// OPA mode: fetch credentials via token exchange (per-user-session)
377385
if (isOPAConfigured()) {
378-
console.log('\n🔐 OPA secret management detected, fetching credentials via token exchange...');
379-
380386
try {
381387
const opaCredentials = await fetchLLMCredentialsFromOPA(idToken);
382388

383389
if (opaCredentials) {
384-
// Build config from OPA credentials
385390
const baseConfig = {
386391
mcpServerUrl: process.env.MCP_SERVER_URL || '',
387392
name: 'agent0',
@@ -412,7 +417,6 @@ export async function getAgentForUserContext(idToken: string, userContext: UserC
412417
}
413418

414419
llmConfigSource = 'opa';
415-
console.log('✅ Agent configured with OPA credentials');
416420
return new Agent(agentConfig);
417421
}
418422
} catch (error: any) {

scripts/bootstrap-okta-tenant.js

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ora from 'ora';
55
import * as path from 'path';
66
import { OktaAPIClient } from './lib/okta-api.js';
77
import { generateRSAKeyPair, savePrivateKey } from './lib/key-generator.js';
8-
import { generateAgent0AppEnv, generateAgent0AgentEnv, generateTodo0AppEnv, generateTodo0McpEnv, writeEnvFile, writeConfigReport, } from './lib/env-writer.js';
8+
import { generateAgent0AppEnv, generateAgent0AgentEnv, generateTodo0AppEnv, generateTodo0McpEnv, generateOpaEnvStub, writeEnvFile, writeConfigReport, } from './lib/env-writer.js';
99
import { loadRollbackState, updateRollbackState, } from './lib/state-manager.js';
1010
import { AgentIdentityAPIClient, convertPublicKeyToJWK, constructAuthServerORN, } from './lib/agent-identity-api.js';
1111
/**
@@ -300,9 +300,9 @@ async function bootstrap() {
300300
const connection = await agentClient.createConnection(agentIdentityId, {
301301
connectionType: 'IDENTITY_ASSERTION_CUSTOM_AS',
302302
authorizationServer: {
303-
orn: authServerOrn,
304-
resourceIndicator: config.mcpAudience,
303+
orn: authServerOrn
305304
},
305+
resourceIndicator: config.mcpAudience,
306306
scopeCondition: 'INCLUDE_ONLY',
307307
scopes: mcpScopes,
308308
});
@@ -378,8 +378,126 @@ async function bootstrap() {
378378
else {
379379
spinner.succeed(`Trusted origins configured (${createdOrigins.length} created, ${origins.length - createdOrigins.length} already existed)`);
380380
}
381-
// Step 13: Generate Configuration Files
382-
console.log(chalk.bold('\n📋 Step 13: Generating Configuration Files'));
381+
// Step 13: Configure LLM Credentials
382+
console.log(chalk.bold('\n📋 Step 13: Configure LLM Credentials'));
383+
const llmModeAnswer = await prompts({
384+
type: 'select',
385+
name: 'mode',
386+
message: 'How would you like to configure LLM credentials?',
387+
choices: [
388+
{
389+
title: 'Environment Variables (Simple)',
390+
value: 'env',
391+
description: 'Store API key in .env.agent file',
392+
},
393+
{
394+
title: 'Okta Privileged Access (Secure)',
395+
value: 'opa',
396+
description: 'Fetch credentials from OPA vault per user session',
397+
},
398+
{
399+
title: 'Skip (Manual Setup)',
400+
value: 'skip',
401+
description: 'Configure .env.agent or .env.opa later',
402+
},
403+
],
404+
initial: 0,
405+
});
406+
let llmConfig = { provider: 'skip' };
407+
if (llmModeAnswer.mode === 'env') {
408+
// Direct environment variable mode
409+
const providerAnswer = await prompts({
410+
type: 'select',
411+
name: 'provider',
412+
message: 'Which LLM provider?',
413+
choices: [
414+
{ title: 'Anthropic (Claude)', value: 'anthropic' },
415+
{ title: 'AWS Bedrock', value: 'bedrock' },
416+
],
417+
});
418+
if (providerAnswer.provider === 'anthropic') {
419+
const anthropicAnswers = await prompts([
420+
{
421+
type: 'password',
422+
name: 'apiKey',
423+
message: 'Anthropic API Key:',
424+
validate: (v) => (v && v.startsWith('sk-ant-')) || 'API key must start with sk-ant-',
425+
},
426+
{
427+
type: 'text',
428+
name: 'model',
429+
message: 'Model ID:',
430+
initial: 'claude-sonnet-4-20250514',
431+
},
432+
]);
433+
llmConfig = {
434+
provider: 'anthropic',
435+
apiKey: anthropicAnswers.apiKey,
436+
model: anthropicAnswers.model,
437+
};
438+
}
439+
else if (providerAnswer.provider === 'bedrock') {
440+
const bedrockAnswers = await prompts([
441+
{
442+
type: 'text',
443+
name: 'region',
444+
message: 'AWS Region:',
445+
initial: 'us-east-1',
446+
},
447+
{
448+
type: 'password',
449+
name: 'accessKeyId',
450+
message: 'AWS Access Key ID:',
451+
validate: (v) => !!v || 'Required',
452+
},
453+
{
454+
type: 'password',
455+
name: 'secretAccessKey',
456+
message: 'AWS Secret Access Key:',
457+
validate: (v) => !!v || 'Required',
458+
},
459+
{
460+
type: 'password',
461+
name: 'sessionToken',
462+
message: 'AWS Session Token (optional, press Enter to skip):',
463+
},
464+
{
465+
type: 'text',
466+
name: 'modelId',
467+
message: 'Bedrock Model ID:',
468+
initial: 'anthropic.claude-3-sonnet-20240229-v1:0',
469+
},
470+
]);
471+
llmConfig = {
472+
provider: 'bedrock',
473+
region: bedrockAnswers.region,
474+
accessKeyId: bedrockAnswers.accessKeyId,
475+
secretAccessKey: bedrockAnswers.secretAccessKey,
476+
sessionToken: bedrockAnswers.sessionToken || undefined,
477+
modelId: bedrockAnswers.modelId,
478+
};
479+
}
480+
}
481+
else if (llmModeAnswer.mode === 'opa') {
482+
// OPA mode - ask which provider they'll use
483+
const opaProviderAnswer = await prompts({
484+
type: 'select',
485+
name: 'llmProvider',
486+
message: 'Which LLM provider will you store in OPA?',
487+
choices: [
488+
{ title: 'Anthropic (Claude)', value: 'anthropic' },
489+
{ title: 'AWS Bedrock', value: 'bedrock' },
490+
],
491+
});
492+
llmConfig = {
493+
provider: 'opa',
494+
llmProvider: opaProviderAnswer.llmProvider,
495+
};
496+
}
497+
// Store LLM config for env generation
498+
bootstrapConfig.llmConfig = llmConfig;
499+
// Step 14: Generate Configuration Files
500+
console.log(chalk.bold('\n📋 Step 14: Generating Configuration Files'));
383501
spinner = ora('Writing .env files...').start();
384502
const agent0AppEnv = generateAgent0AppEnv(bootstrapConfig);
385503
writeEnvFile('packages/agent0/.env.app', agent0AppEnv);
@@ -389,16 +507,35 @@ async function bootstrap() {
389507
writeEnvFile('packages/todo0/.env.app', todo0AppEnv);
390508
const todo0McpEnv = generateTodo0McpEnv(bootstrapConfig);
391509
writeEnvFile('packages/todo0/.env.mcp', todo0McpEnv);
510+
// Generate .env.opa stub if OPA mode selected
511+
if (llmConfig.provider === 'opa') {
512+
const opaEnvStub = generateOpaEnvStub(llmConfig.llmProvider);
513+
writeEnvFile('packages/agent0/.env.opa', opaEnvStub);
514+
}
392515
writeConfigReport(bootstrapConfig);
393516
spinner.succeed('Configuration files generated');
394517
// Success!
395518
console.log(chalk.bold.green('\n✅ Bootstrap Complete!\n'));
396-
console.log('Next steps:');
397-
console.log(` 1. ${chalk.cyan('pnpm install')} - Install dependencies`);
398-
console.log(` 2. ${chalk.cyan('pnpm run bootstrap')} - Bootstrap database`);
399-
console.log(` 3. ${chalk.cyan('pnpm run start:todo0')} - Start REST API`);
400-
console.log(` 4. ${chalk.cyan('pnpm run start:mcp')} - Start MCP Server`);
401-
console.log(` 5. ${chalk.cyan('pnpm run start:agent0')} - Start Agent`);
519+
if (llmConfig.provider === 'opa') {
520+
// OPA mode - show OPA-specific next steps
521+
console.log(chalk.bold('Next steps for OPA mode:\n'));
522+
console.log(` ${chalk.cyan('1.')} ${chalk.white('pnpm install')} - Install dependencies`);
523+
console.log(` ${chalk.cyan('2.')} ${chalk.white('pnpm run bootstrap')} - Bootstrap database`);
524+
console.log(` ${chalk.cyan('3.')} ${chalk.yellow('pnpm run setup:opa')} - Create secrets in OPA vault`);
525+
console.log(` ${chalk.cyan('4.')} ${chalk.yellow('pnpm run link:opa')} - Connect agent to OPA secrets`);
526+
console.log(` ${chalk.cyan('5.')} ${chalk.white('pnpm run dev')} - Start all services`);
527+
console.log(chalk.gray('\n Note: Steps 3-4 configure OPA secret management'));
528+
}
529+
else {
530+
// Direct mode or skip - show standard next steps
531+
console.log('Next steps:');
532+
console.log(` 1. ${chalk.cyan('pnpm install')} - Install dependencies`);
533+
console.log(` 2. ${chalk.cyan('pnpm run bootstrap')} - Bootstrap database`);
534+
console.log(` 3. ${chalk.cyan('pnpm run dev')} - Start all services`);
535+
if (llmConfig.provider === 'skip') {
536+
console.log(chalk.yellow('\n ⚠️ Remember to configure LLM credentials in .env.agent'));
537+
}
538+
}
402539
console.log(`\n Optional: ${chalk.cyan('pnpm run validate:okta')} - Validate configuration`);
403540
console.log(`\n📄 See ${chalk.cyan('okta-config-report.md')} for detailed configuration\n`);
404541
}

0 commit comments

Comments
 (0)