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
27 changes: 21 additions & 6 deletions src/cli/tui/screens/identity/AddIdentityFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,26 @@ export function AddIdentityFlow({ isInteractive = true, onExit, onBack, onDev, o

const handleCreateComplete = useCallback(
(config: AddIdentityConfig) => {
void createIdentity({
type: 'ApiKeyCredentialProvider',
name: config.name,
apiKey: config.apiKey,
}).then(result => {
const createConfig =
config.identityType === 'OAuthCredentialProvider'
? {
type: 'OAuthCredentialProvider' as const,
name: config.name,
discoveryUrl: config.discoveryUrl!,
clientId: config.clientId!,
clientSecret: config.clientSecret!,
scopes: config.scopes
?.split(',')
.map(s => s.trim())
.filter(Boolean),
}
: {
type: 'ApiKeyCredentialProvider' as const,
name: config.name,
apiKey: config.apiKey,
};

void createIdentity(createConfig).then(result => {
if (result.ok) {
setFlow({ name: 'create-success', identityName: result.result.name });
return;
Expand All @@ -63,7 +78,7 @@ export function AddIdentityFlow({ isInteractive = true, onExit, onBack, onDev, o
<AddSuccessScreen
isInteractive={isInteractive}
message={`Added credential: ${flow.identityName}`}
detail="Credential added to project in `agentcore/agentcore.json`. API key stored in `agentcore/.env.local`."
detail="Credential added to project in `agentcore/agentcore.json`. Secrets stored in `agentcore/.env.local`."
showDevOption={true}
onAddAnother={onBack}
onDev={onDev}
Expand Down
89 changes: 82 additions & 7 deletions src/cli/tui/screens/identity/AddIdentityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export function AddIdentityScreen({ onComplete, onExit, existingIdentityNames }:
const isTypeStep = wizard.step === 'type';
const isNameStep = wizard.step === 'name';
const isApiKeyStep = wizard.step === 'apiKey';
const isDiscoveryUrlStep = wizard.step === 'discoveryUrl';
const isClientIdStep = wizard.step === 'clientId';
const isClientSecretStep = wizard.step === 'clientSecret';
const isScopesStep = wizard.step === 'scopes';
const isConfirmStep = wizard.step === 'confirm';
const isOAuth = wizard.config.identityType === 'OAuthCredentialProvider';

const typeNav = useListNavigation({
items: typeItems,
Expand All @@ -51,6 +56,10 @@ export function AddIdentityScreen({ onComplete, onExit, existingIdentityNames }:

const headerContent = <StepIndicator steps={wizard.steps} currentStep={wizard.step} labels={IDENTITY_STEP_LABELS} />;

const defaultName = isOAuth
? generateUniqueName('MyOAuth', existingIdentityNames)
: generateUniqueName('MyApiKey', existingIdentityNames);

return (
<Screen title="Add Credential" onExit={onExit} helpText={helpText} headerContent={headerContent}>
<Panel>
Expand All @@ -67,10 +76,11 @@ export function AddIdentityScreen({ onComplete, onExit, existingIdentityNames }:
<TextInput
key="name"
prompt="Credential name"
initialValue={generateUniqueName('MyApiKey', existingIdentityNames)}
initialValue={defaultName}
onSubmit={wizard.setName}
onCancel={() => wizard.goBack()}
schema={CredentialNameSchema}
customValidation={value => !existingIdentityNames.includes(value) || 'Credential name already exists'}
/>
)}

Expand All @@ -85,16 +95,81 @@ export function AddIdentityScreen({ onComplete, onExit, existingIdentityNames }:
/>
)}

{isDiscoveryUrlStep && (
<TextInput
key="discoveryUrl"
prompt="Discovery URL (OIDC well-known endpoint)"
initialValue="https://"
onSubmit={wizard.setDiscoveryUrl}
onCancel={() => wizard.goBack()}
customValidation={value => {
try {
new URL(value);
} catch {
return 'Must be a valid URL';
}
if (!value.endsWith('/.well-known/openid-configuration')) {
return "URL must end with '/.well-known/openid-configuration'";
}
return true;
}}
/>
)}

{isClientIdStep && (
<SecretInput
key="clientId"
prompt="Client ID"
onSubmit={wizard.setClientId}
onCancel={() => wizard.goBack()}
customValidation={value => value.trim().length > 0 || 'Client ID is required'}
revealChars={4}
/>
)}

{isClientSecretStep && (
<SecretInput
key="clientSecret"
prompt="Client Secret"
onSubmit={wizard.setClientSecret}
onCancel={() => wizard.goBack()}
customValidation={value => value.trim().length > 0 || 'Client secret is required'}
revealChars={4}
/>
)}

{isScopesStep && (
<TextInput
key="scopes"
prompt="Scopes (comma-separated, optional)"
placeholder="press Enter to skip"
initialValue=""
onSubmit={wizard.setScopes}
onCancel={() => wizard.goBack()}
allowEmpty
/>
)}

{isConfirmStep && (
<ConfirmReview
fields={[
{ label: 'Type', value: 'API Key' },
{ label: 'Name', value: wizard.config.name },
{ label: 'API Key', value: '*'.repeat(Math.min(wizard.config.apiKey.length, 20)) },
]}
fields={
isOAuth
? [
{ label: 'Type', value: 'OAuth' },
{ label: 'Name', value: wizard.config.name },
{ label: 'Discovery URL', value: wizard.config.discoveryUrl ?? '' },
{ label: 'Client ID', value: wizard.config.clientId ? '****' + wizard.config.clientId.slice(-4) : '' },
...(wizard.config.scopes ? [{ label: 'Scopes', value: wizard.config.scopes }] : []),
]
: [
{ label: 'Type', value: 'API Key' },
{ label: 'Name', value: wizard.config.name },
{ label: 'API Key', value: '*'.repeat(Math.min(wizard.config.apiKey.length, 20)) },
]
}
/>
)}
</Panel>
</Screen>
);
}
}
23 changes: 21 additions & 2 deletions src/cli/tui/screens/identity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,36 @@ import type { CredentialType } from '../../../../schema';
// Identity Flow Types
// ─────────────────────────────────────────────────────────────────────────────

export type AddIdentityStep = 'type' | 'name' | 'apiKey' | 'confirm';
export type AddIdentityStep =
| 'type'
| 'name'
| 'apiKey'
| 'discoveryUrl'
| 'clientId'
| 'clientSecret'
| 'scopes'
| 'confirm';

export interface AddIdentityConfig {
identityType: CredentialType;
name: string;
/** API Key (when type is ApiKeyCredentialProvider) */
apiKey: string;
/** OAuth fields (when type is OAuthCredentialProvider) */
discoveryUrl?: string;
clientId?: string;
clientSecret?: string;
scopes?: string;
}

export const IDENTITY_STEP_LABELS: Record<AddIdentityStep, string> = {
type: 'Type',
name: 'Name',
apiKey: 'API Key',
discoveryUrl: 'Discovery URL',
clientId: 'Client ID',
clientSecret: 'Client Secret',
scopes: 'Scopes',
confirm: 'Confirm',
};

Expand All @@ -25,4 +43,5 @@ export const IDENTITY_STEP_LABELS: Record<AddIdentityStep, string> = {

export const IDENTITY_TYPE_OPTIONS = [
{ id: 'ApiKeyCredentialProvider' as const, title: 'API Key', description: 'Store and manage API key credentials' },
] as const;
{ id: 'OAuthCredentialProvider' as const, title: 'OAuth', description: 'OAuth 2.0 client credentials' },
] as const;
95 changes: 72 additions & 23 deletions src/cli/tui/screens/identity/useAddIdentityWizard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { CredentialType } from '../../../../schema';
import type { AddIdentityConfig, AddIdentityStep } from './types';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';

const ALL_STEPS: AddIdentityStep[] = ['type', 'name', 'apiKey', 'confirm'];
function getSteps(identityType: CredentialType): AddIdentityStep[] {
if (identityType === 'OAuthCredentialProvider') {
return ['type', 'name', 'discoveryUrl', 'clientId', 'clientSecret', 'scopes', 'confirm'];
}
return ['type', 'name', 'apiKey', 'confirm'];
}

function getDefaultConfig(): AddIdentityConfig {
return {
Expand All @@ -16,43 +21,83 @@ export function useAddIdentityWizard() {
const [config, setConfig] = useState<AddIdentityConfig>(getDefaultConfig);
const [step, setStep] = useState<AddIdentityStep>('type');

const currentIndex = ALL_STEPS.indexOf(step);
const steps = useMemo(() => getSteps(config.identityType), [config.identityType]);
const currentIndex = steps.indexOf(step);

const goBack = useCallback(() => {
const prevStep = ALL_STEPS[currentIndex - 1];
const prevStep = steps[currentIndex - 1];
if (prevStep) setStep(prevStep);
}, [currentIndex]);

const nextStep = useCallback((currentStep: AddIdentityStep): AddIdentityStep | undefined => {
const idx = ALL_STEPS.indexOf(currentStep);
return ALL_STEPS[idx + 1];
}, []);
}, [currentIndex, steps]);

const setIdentityType = useCallback(
(identityType: CredentialType) => {
setConfig(c => ({ ...c, identityType }));
const next = nextStep('type');
const advanceFrom = useCallback(
(currentStep: AddIdentityStep) => {
const currentSteps = getSteps(config.identityType);
const idx = currentSteps.indexOf(currentStep);
const next = currentSteps[idx + 1];
if (next) setStep(next);
},
[nextStep]
[config.identityType]
);

const setIdentityType = useCallback((identityType: CredentialType) => {
setConfig(c => ({
...c,
identityType,
apiKey: '',
discoveryUrl: undefined,
clientId: undefined,
clientSecret: undefined,
scopes: undefined,
}));
setStep('name');
}, []);

const setName = useCallback(
(name: string) => {
setConfig(c => ({ ...c, name }));
const next = nextStep('name');
if (next) setStep(next);
advanceFrom('name');
},
[nextStep]
[advanceFrom]
);

const setApiKey = useCallback(
(apiKey: string) => {
setConfig(c => ({ ...c, apiKey }));
const next = nextStep('apiKey');
if (next) setStep(next);
advanceFrom('apiKey');
},
[advanceFrom]
);

const setDiscoveryUrl = useCallback(
(discoveryUrl: string) => {
setConfig(c => ({ ...c, discoveryUrl }));
advanceFrom('discoveryUrl');
},
[nextStep]
[advanceFrom]
);

const setClientId = useCallback(
(clientId: string) => {
setConfig(c => ({ ...c, clientId }));
advanceFrom('clientId');
},
[advanceFrom]
);

const setClientSecret = useCallback(
(clientSecret: string) => {
setConfig(c => ({ ...c, clientSecret }));
advanceFrom('clientSecret');
},
[advanceFrom]
);

const setScopes = useCallback(
(scopes: string) => {
setConfig(c => ({ ...c, scopes: scopes || undefined }));
advanceFrom('scopes');
},
[advanceFrom]
);

const reset = useCallback(() => {
Expand All @@ -63,12 +108,16 @@ export function useAddIdentityWizard() {
return {
config,
step,
steps: ALL_STEPS,
steps,
currentIndex,
goBack,
setIdentityType,
setName,
setApiKey,
setDiscoveryUrl,
setClientId,
setClientSecret,
setScopes,
reset,
};
}
}
Loading