Skip to content

Commit f4040ee

Browse files
Hweinstockjesseturner21
authored andcommitted
feat: add policy engine selection to gateway TUI wizard
1 parent ad88e2c commit f4040ee

14 files changed

Lines changed: 493 additions & 40 deletions

File tree

src/cli/commands/remove/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
export type ResourceType = 'agent' | 'gateway' | 'gateway-target' | 'memory' | 'identity' | 'evaluator' | 'online-eval' | 'policy-engine' | 'policy';
1+
export type ResourceType =
2+
| 'agent'
3+
| 'gateway'
4+
| 'gateway-target'
5+
| 'memory'
6+
| 'identity'
7+
| 'evaluator'
8+
| 'online-eval'
9+
| 'policy-engine'
10+
| 'policy';
211

312
export interface RemoveOptions {
413
resourceType: ResourceType;

src/cli/commands/status/action.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ import type { ResourceDeploymentState } from './constants';
1515
export type { ResourceDeploymentState };
1616

1717
export interface ResourceStatusEntry {
18-
resourceType: 'agent' | 'memory' | 'credential' | 'gateway' | 'evaluator' | 'online-eval' | 'policy-engine' | 'policy';
18+
resourceType:
19+
| 'agent'
20+
| 'memory'
21+
| 'credential'
22+
| 'gateway'
23+
| 'evaluator'
24+
| 'online-eval'
25+
| 'policy-engine'
26+
| 'policy';
1927
name: string;
2028
deploymentState: ResourceDeploymentState;
2129
identifier?: string;
@@ -201,7 +209,16 @@ export function computeResourceStatuses(
201209
getDeployedKey: item => `${item.engineName}/${item.name}`,
202210
});
203211

204-
return [...agents, ...credentials, ...memories, ...gateways, ...evaluators, ...onlineEvalConfigs, ...policyEngines, ...policies];
212+
return [
213+
...agents,
214+
...credentials,
215+
...memories,
216+
...gateways,
217+
...evaluators,
218+
...onlineEvalConfigs,
219+
...policyEngines,
220+
...policies,
221+
];
205222
}
206223

207224
export async function handleProjectStatus(

src/cli/commands/status/command.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ import { DEPLOYMENT_STATE_COLORS, DEPLOYMENT_STATE_LABELS } from './constants';
77
import type { Command } from '@commander-js/extra-typings';
88
import { Box, Text, render } from 'ink';
99

10-
const VALID_RESOURCE_TYPES = ['agent', 'memory', 'credential', 'gateway', 'evaluator', 'online-eval', 'policy-engine', 'policy'] as const;
10+
const VALID_RESOURCE_TYPES = [
11+
'agent',
12+
'memory',
13+
'credential',
14+
'gateway',
15+
'evaluator',
16+
'online-eval',
17+
'policy-engine',
18+
'policy',
19+
] as const;
1120
const VALID_STATES = ['deployed', 'local-only', 'pending-removal'] as const;
1221

1322
interface StatusCliOptions {

src/cli/logging/remove-logger.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ const REMOVE_LOGS_SUBDIR = 'remove';
77

88
export interface RemoveLoggerOptions {
99
/** Type of resource being removed */
10-
resourceType: 'agent' | 'memory' | 'identity' | 'gateway' | 'gateway-target' | 'evaluator' | 'online-eval' | 'policy-engine' | 'policy';
10+
resourceType:
11+
| 'agent'
12+
| 'memory'
13+
| 'identity'
14+
| 'gateway'
15+
| 'gateway-target'
16+
| 'evaluator'
17+
| 'online-eval'
18+
| 'policy-engine'
19+
| 'policy';
1120
/** Name of the resource being removed */
1221
resourceName: string;
1322
}

src/cli/primitives/PolicyEnginePrimitive.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { findConfigRoot } from '../../lib';
22
import type { AgentCoreProjectSpec, PolicyEngine } from '../../schema';
3-
import { PolicyEngineSchema } from '../../schema';
3+
import { PolicyEngineModeSchema, PolicyEngineSchema } from '../../schema';
44
import { getErrorMessage } from '../errors';
55
import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types';
66
import { BasePrimitive } from './BasePrimitive';
@@ -149,6 +149,34 @@ export class PolicyEnginePrimitive extends BasePrimitive<AddPolicyEngineOptions,
149149
}
150150
}
151151

152+
/**
153+
* Get gateway names that don't have a policy engine attached.
154+
*/
155+
async getUnprotectedGateways(): Promise<string[]> {
156+
try {
157+
if (!this.configIO.configExists('mcp')) return [];
158+
const mcpSpec = await this.configIO.readMcpSpec();
159+
return mcpSpec.agentCoreGateways.filter(gw => !gw.policyEngineConfiguration).map(gw => gw.name);
160+
} catch {
161+
return [];
162+
}
163+
}
164+
165+
/**
166+
* Attach a policy engine to the specified gateways in mcp.json.
167+
*/
168+
async attachToGateways(engineName: string, gatewayNames: string[], mode: 'LOG_ONLY' | 'ENFORCE'): Promise<void> {
169+
if (gatewayNames.length === 0 || !this.configIO.configExists('mcp')) return;
170+
const mcpSpec = await this.configIO.readMcpSpec();
171+
const nameSet = new Set(gatewayNames);
172+
for (const gw of mcpSpec.agentCoreGateways) {
173+
if (nameSet.has(gw.name)) {
174+
gw.policyEngineConfiguration = { policyEngineName: engineName, mode };
175+
}
176+
}
177+
await this.configIO.writeMcpSpec(mcpSpec);
178+
}
179+
152180
async getDeployedEngineId(engineName: string): Promise<string | null> {
153181
try {
154182
const deployedState = await this.configIO.readDeployedState();
@@ -197,9 +225,18 @@ export class PolicyEnginePrimitive extends BasePrimitive<AddPolicyEngineOptions,
197225
.option('--name <name>', 'Policy engine name [non-interactive]')
198226
.option('--description <desc>', 'Policy engine description [non-interactive]')
199227
.option('--encryption-key-arn <arn>', 'KMS encryption key ARN [non-interactive]')
228+
.option('--attach-to-gateways <gateways>', 'Comma-separated gateway names to attach this engine to')
229+
.option('--attach-mode <mode>', 'Enforcement mode for attached gateways: LOG_ONLY or ENFORCE')
200230
.option('--json', 'Output as JSON [non-interactive]')
201231
.action(
202-
async (cliOptions: { name?: string; description?: string; encryptionKeyArn?: string; json?: boolean }) => {
232+
async (cliOptions: {
233+
name?: string;
234+
description?: string;
235+
encryptionKeyArn?: string;
236+
attachToGateways?: string;
237+
attachMode?: string;
238+
json?: boolean;
239+
}) => {
203240
try {
204241
if (!findConfigRoot()) {
205242
console.error('No agentcore project found. Run `agentcore create` first.');
@@ -222,6 +259,16 @@ export class PolicyEnginePrimitive extends BasePrimitive<AddPolicyEngineOptions,
222259
encryptionKeyArn: cliOptions.encryptionKeyArn,
223260
});
224261

262+
// Attach to gateways if requested
263+
if (result.success && cliOptions.attachToGateways) {
264+
const mode = PolicyEngineModeSchema.parse(cliOptions.attachMode ?? 'LOG_ONLY');
265+
const gateways = cliOptions.attachToGateways
266+
.split(',')
267+
.map(s => s.trim())
268+
.filter(Boolean);
269+
await this.attachToGateways(cliOptions.name, gateways, mode);
270+
}
271+
225272
if (cliOptions.json) {
226273
console.log(JSON.stringify(result));
227274
} else if (result.success) {

src/cli/tui/hooks/useCreateMcp.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { agentPrimitive, gatewayPrimitive, gatewayTargetPrimitive } from '../../primitives/registry';
1+
import {
2+
agentPrimitive,
3+
gatewayPrimitive,
4+
gatewayTargetPrimitive,
5+
policyEnginePrimitive,
6+
} from '../../primitives/registry';
27
import type { AddGatewayConfig } from '../screens/mcp/types';
38
import { useCallback, useEffect, useState } from 'react';
49

@@ -30,6 +35,8 @@ export function useCreateGateway() {
3035
agentClientSecret: config.jwtConfig?.agentClientSecret,
3136
enableSemanticSearch: config.enableSemanticSearch,
3237
exceptionLevel: config.exceptionLevel,
38+
policyEngine: config.policyEngineConfiguration?.policyEngineName,
39+
policyEngineMode: config.policyEngineConfiguration?.mode,
3340
});
3441
if (!addResult.success) {
3542
throw new Error(addResult.error ?? 'Failed to create gateway');
@@ -70,6 +77,25 @@ export function useExistingGateways() {
7077
return { gateways, refresh };
7178
}
7279

80+
export function useExistingPolicyEngines() {
81+
const [engines, setEngines] = useState<string[]>([]);
82+
83+
useEffect(() => {
84+
async function load() {
85+
const result = await policyEnginePrimitive.getExistingEngines();
86+
setEngines(result);
87+
}
88+
void load();
89+
}, []);
90+
91+
const refresh = useCallback(async () => {
92+
const result = await policyEnginePrimitive.getExistingEngines();
93+
setEngines(result);
94+
}, []);
95+
96+
return { engines, refresh };
97+
}
98+
7399
export function useAvailableAgents() {
74100
const [agents, setAgents] = useState<string[] | null>(null);
75101

src/cli/tui/screens/mcp/AddGatewayFlow.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { ErrorPrompt } from '../../components';
2-
import { useCreateGateway, useExistingGateways, useUnassignedTargets } from '../../hooks/useCreateMcp';
2+
import {
3+
useCreateGateway,
4+
useExistingGateways,
5+
useExistingPolicyEngines,
6+
useUnassignedTargets,
7+
} from '../../hooks/useCreateMcp';
38
import { AddSuccessScreen } from '../add/AddSuccessScreen';
49
import { AddGatewayScreen } from './AddGatewayScreen';
510
import type { AddGatewayConfig } from './types';
@@ -25,6 +30,7 @@ export function AddGatewayFlow({ isInteractive = true, onExit, onBack, onDev, on
2530
const { createGateway, reset: resetCreate } = useCreateGateway();
2631
const { gateways: existingGateways, refresh: refreshGateways } = useExistingGateways();
2732
const { targets: unassignedTargets } = useUnassignedTargets();
33+
const { engines: existingPolicyEngines } = useExistingPolicyEngines();
2834
const [flow, setFlow] = useState<FlowState>({ name: 'create-wizard' });
2935

3036
// In non-interactive mode, exit after success (but not while loading)
@@ -61,6 +67,7 @@ export function AddGatewayFlow({ isInteractive = true, onExit, onBack, onDev, on
6167
<AddGatewayScreen
6268
existingGateways={existingGateways}
6369
unassignedTargets={unassignedTargets}
70+
existingPolicyEngines={existingPolicyEngines}
6471
onComplete={handleCreateComplete}
6572
onExit={onBack}
6673
/>

src/cli/tui/screens/mcp/AddGatewayScreen.tsx

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GatewayAuthorizerType } from '../../../../schema';
1+
import type { GatewayAuthorizerType, PolicyEngineMode } from '../../../../schema';
22
import { GatewayNameSchema } from '../../../../schema';
33
import { computeManagedOAuthCredentialName } from '../../../primitives/credential-utils';
44
import {
@@ -20,6 +20,8 @@ import {
2020
AUTHORIZER_TYPE_OPTIONS,
2121
EXCEPTION_LEVEL_ITEM_ID,
2222
GATEWAY_STEP_LABELS,
23+
NONE_SELECTION,
24+
POLICY_ENGINE_MODE_OPTIONS,
2325
SEMANTIC_SEARCH_ITEM_ID,
2426
} from './types';
2527
import { useAddGatewayWizard } from './useAddGatewayWizard';
@@ -31,12 +33,19 @@ interface AddGatewayScreenProps {
3133
onExit: () => void;
3234
existingGateways: string[];
3335
unassignedTargets: string[];
36+
existingPolicyEngines: string[];
3437
}
3538

3639
const INITIAL_ADVANCED_SELECTED = [SEMANTIC_SEARCH_ITEM_ID];
3740

38-
export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassignedTargets }: AddGatewayScreenProps) {
39-
const wizard = useAddGatewayWizard(unassignedTargets.length);
41+
export function AddGatewayScreen({
42+
onComplete,
43+
onExit,
44+
existingGateways,
45+
unassignedTargets,
46+
existingPolicyEngines,
47+
}: AddGatewayScreenProps) {
48+
const wizard = useAddGatewayWizard(unassignedTargets.length, existingPolicyEngines.length);
4049

4150
// JWT config sub-step tracking (0=discoveryUrl, 1=audience, 2=clients, 3=scopes, 4=agentClientId, 5=agentClientSecret)
4251
const [jwtSubStep, setJwtSubStep] = useState(0);
@@ -64,10 +73,35 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
6473
[]
6574
);
6675

76+
// Policy engine sub-step: 0 = select engine, 1 = select mode
77+
// Reset when re-entering the step (e.g., after navigating back)
78+
const [policyEngineSubStep, setPolicyEngineSubStep] = useState(0);
79+
const [selectedEngineName, setSelectedEngineName] = useState('');
80+
React.useEffect(() => {
81+
if (wizard.step === 'policy-engine') {
82+
setPolicyEngineSubStep(0);
83+
setSelectedEngineName('');
84+
}
85+
}, [wizard.step]);
86+
87+
const policyEngineItems: SelectableItem[] = useMemo(
88+
() => [
89+
{ id: NONE_SELECTION, title: 'None', description: 'No policy engine' },
90+
...existingPolicyEngines.map(name => ({ id: name, title: name })),
91+
],
92+
[existingPolicyEngines]
93+
);
94+
95+
const policyEngineModeItems: SelectableItem[] = useMemo(
96+
() => POLICY_ENGINE_MODE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })),
97+
[]
98+
);
99+
67100
const isNameStep = wizard.step === 'name';
68101
const isAuthorizerStep = wizard.step === 'authorizer';
69102
const isJwtConfigStep = wizard.step === 'jwt-config';
70103
const isIncludeTargetsStep = wizard.step === 'include-targets';
104+
const isPolicyEngineStep = wizard.step === 'policy-engine';
71105
const isAdvancedConfigStep = wizard.step === 'advanced-config';
72106
const isConfirmStep = wizard.step === 'confirm';
73107

@@ -87,6 +121,36 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
87121
requireSelection: false,
88122
});
89123

124+
const policyEngineNav = useListNavigation({
125+
items: policyEngineItems,
126+
onSelect: item => {
127+
if (item.id === NONE_SELECTION) {
128+
wizard.skipPolicyEngine();
129+
} else {
130+
setSelectedEngineName(item.id);
131+
setPolicyEngineSubStep(1);
132+
}
133+
},
134+
onExit: () => {
135+
if (policyEngineSubStep === 0) {
136+
wizard.goBack();
137+
} else {
138+
setPolicyEngineSubStep(0);
139+
}
140+
},
141+
isActive: isPolicyEngineStep && policyEngineSubStep === 0,
142+
});
143+
144+
const policyEngineModeNav = useListNavigation({
145+
items: policyEngineModeItems,
146+
onSelect: item => {
147+
wizard.setPolicyEngineConfig(selectedEngineName, item.id as PolicyEngineMode);
148+
setPolicyEngineSubStep(0);
149+
},
150+
onExit: () => setPolicyEngineSubStep(0),
151+
isActive: isPolicyEngineStep && policyEngineSubStep === 1,
152+
});
153+
90154
const advancedNav = useMultiSelectNavigation({
91155
items: advancedConfigItems,
92156
getId: item => item.id,
@@ -172,7 +236,7 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
172236
? 'Space toggle · Enter confirm · Esc back'
173237
: isConfirmStep
174238
? HELP_TEXT.CONFIRM_CANCEL
175-
: isAuthorizerStep
239+
: isAuthorizerStep || isPolicyEngineStep
176240
? HELP_TEXT.NAVIGATE_SELECT
177241
: HELP_TEXT.TEXT_INPUT;
178242

@@ -234,6 +298,24 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
234298
<Text dimColor>No unassigned targets available. Press Enter to continue.</Text>
235299
))}
236300

301+
{isPolicyEngineStep && policyEngineSubStep === 0 && (
302+
<WizardSelect
303+
title="Select a policy engine"
304+
description="Attach a Cedar policy engine to authorize tool calls on this gateway"
305+
items={policyEngineItems}
306+
selectedIndex={policyEngineNav.selectedIndex}
307+
/>
308+
)}
309+
310+
{isPolicyEngineStep && policyEngineSubStep === 1 && (
311+
<WizardSelect
312+
title="Select enforcement mode"
313+
description={`Policy engine: ${selectedEngineName}`}
314+
items={policyEngineModeItems}
315+
selectedIndex={policyEngineModeNav.selectedIndex}
316+
/>
317+
)}
318+
237319
{isAdvancedConfigStep && (
238320
<Box flexDirection="column">
239321
<Text bold>Advanced Configuration</Text>
@@ -286,6 +368,12 @@ export function AddGatewayScreen({ onComplete, onExit, existingGateways, unassig
286368
},
287369
{ label: 'Semantic Search', value: wizard.config.enableSemanticSearch ? 'Enabled' : 'Disabled' },
288370
{ label: 'Exception Level', value: wizard.config.exceptionLevel === 'DEBUG' ? 'Debug' : 'None' },
371+
...(wizard.config.policyEngineConfiguration
372+
? [
373+
{ label: 'Policy Engine', value: wizard.config.policyEngineConfiguration.policyEngineName },
374+
{ label: 'Enforcement Mode', value: wizard.config.policyEngineConfiguration.mode },
375+
]
376+
: []),
289377
]}
290378
/>
291379
)}

0 commit comments

Comments
 (0)