Skip to content

Commit 6851956

Browse files
committed
feat: add PolicyEngineConfiguration support for gateways
1 parent 24995ab commit 6851956

File tree

10 files changed

+143
-16
lines changed

10 files changed

+143
-16
lines changed

src/cli/commands/add/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface AddGatewayOptions {
3939
agents?: string;
4040
semanticSearch?: boolean;
4141
exceptionLevel?: string;
42+
policyEngine?: string;
43+
policyEngineMode?: string;
4244
json?: boolean;
4345
}
4446

src/cli/commands/add/validate.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,17 @@ export function validateAddGatewayOptions(options: AddGatewayOptions): Validatio
273273
}
274274
}
275275

276+
// Validate policy engine options
277+
if (options.policyEngine && !options.policyEngineMode) {
278+
return { valid: false, error: '--policy-engine-mode is required when --policy-engine is specified' };
279+
}
280+
if (options.policyEngineMode && !options.policyEngine) {
281+
return { valid: false, error: '--policy-engine is required when --policy-engine-mode is specified' };
282+
}
283+
if (options.policyEngineMode && !['LOG_ONLY', 'ENFORCE'].includes(options.policyEngineMode)) {
284+
return { valid: false, error: `Invalid policy engine mode: ${options.policyEngineMode}. Use LOG_ONLY or ENFORCE` };
285+
}
286+
276287
return { valid: true };
277288
}
278289

src/cli/primitives/GatewayPrimitive.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { findConfigRoot, setEnvVar } from '../../lib';
22
import type { AgentCoreGateway, AgentCoreGatewayTarget, AgentCoreMcpSpec, GatewayAuthorizerType } from '../../schema';
3-
import { AgentCoreGatewaySchema } from '../../schema';
3+
import { AgentCoreGatewaySchema, PolicyEngineModeSchema } from '../../schema';
44
import type { AddGatewayOptions as CLIAddGatewayOptions } from '../commands/add/types';
55
import { validateAddGatewayOptions } from '../commands/add/validate';
66
import { getErrorMessage } from '../errors';
@@ -28,6 +28,8 @@ export interface AddGatewayOptions {
2828
agents?: string;
2929
enableSemanticSearch?: boolean;
3030
exceptionLevel?: string;
31+
policyEngine?: string;
32+
policyEngineMode?: string;
3133
}
3234

3335
/**
@@ -160,6 +162,8 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
160162
.option('--agents <agents>', 'Comma-separated agent names')
161163
.option('--no-semantic-search', 'Disable semantic search for tool discovery')
162164
.option('--exception-level <level>', 'Exception verbosity level', 'NONE')
165+
.option('--policy-engine <name>', 'Policy engine name for Cedar-based authorization')
166+
.option('--policy-engine-mode <mode>', 'Policy engine mode: LOG_ONLY or ENFORCE')
163167
.option('--json', 'Output as JSON')
164168
.action(async (rawOptions: Record<string, string | boolean | undefined>) => {
165169
const cliOptions = rawOptions as unknown as CLIAddGatewayOptions;
@@ -192,6 +196,8 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
192196
agents: cliOptions.agents,
193197
enableSemanticSearch: cliOptions.semanticSearch !== false,
194198
exceptionLevel: cliOptions.exceptionLevel,
199+
policyEngine: cliOptions.policyEngine,
200+
policyEngineMode: cliOptions.policyEngineMode,
195201
});
196202

197203
if (cliOptions.json) {
@@ -290,6 +296,10 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
290296
jwtConfig: undefined,
291297
enableSemanticSearch: options.enableSemanticSearch ?? true,
292298
exceptionLevel: options.exceptionLevel === 'DEBUG' ? 'DEBUG' : 'NONE',
299+
policyEngineConfiguration:
300+
options.policyEngine && options.policyEngineMode
301+
? { policyEngineName: options.policyEngine, mode: PolicyEngineModeSchema.parse(options.policyEngineMode) }
302+
: undefined,
293303
};
294304

295305
if (options.authorizerType === 'CUSTOM_JWT' && options.discoveryUrl) {
@@ -358,6 +368,7 @@ export class GatewayPrimitive extends BasePrimitive<AddGatewayOptions, Removable
358368
authorizerConfiguration: this.buildAuthorizerConfiguration(config),
359369
enableSemanticSearch: config.enableSemanticSearch,
360370
exceptionLevel: config.exceptionLevel,
371+
policyEngineConfiguration: config.policyEngineConfiguration,
361372
};
362373

363374
mcpSpec.agentCoreGateways.push(gateway);

src/cli/primitives/PolicyEnginePrimitive.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ export class PolicyEnginePrimitive extends BasePrimitive<AddPolicyEngineOptions,
5353
project.policyEngines.splice(index, 1);
5454
await this.writeProjectSpec(project);
5555

56+
// Clean up any gateway references to this engine in mcp.json
57+
if (this.configIO.configExists('mcp')) {
58+
const mcpSpec = await this.configIO.readMcpSpec();
59+
let changed = false;
60+
for (const gw of mcpSpec.agentCoreGateways) {
61+
if (gw.policyEngineConfiguration?.policyEngineName === engineName) {
62+
delete gw.policyEngineConfiguration;
63+
changed = true;
64+
}
65+
}
66+
if (changed) {
67+
await this.configIO.writeMcpSpec(mcpSpec);
68+
}
69+
}
70+
5671
return { success: true };
5772
} catch (err) {
5873
const message = err instanceof Error ? err.message : 'Unknown error';
@@ -84,6 +99,35 @@ export class PolicyEnginePrimitive extends BasePrimitive<AddPolicyEngineOptions,
8499
after: afterSpec,
85100
});
86101

102+
// Show mcp.json changes if any gateways reference this engine
103+
if (this.configIO.configExists('mcp')) {
104+
const mcpSpec = await this.configIO.readMcpSpec();
105+
const affectedGateways = mcpSpec.agentCoreGateways.filter(
106+
gw => gw.policyEngineConfiguration?.policyEngineName === engineName
107+
);
108+
if (affectedGateways.length > 0) {
109+
summary.push(
110+
`Note: ${affectedGateways.length} gateway(s) referencing this engine will have policyEngineConfiguration removed`
111+
);
112+
summary.push(
113+
'Warning: this may grant agents escalated permissions to invoke gateway tools that were previously restricted'
114+
);
115+
const afterMcpSpec = {
116+
...mcpSpec,
117+
agentCoreGateways: mcpSpec.agentCoreGateways.map(gw =>
118+
gw.policyEngineConfiguration?.policyEngineName === engineName
119+
? { ...gw, policyEngineConfiguration: undefined }
120+
: gw
121+
),
122+
};
123+
schemaChanges.push({
124+
file: 'agentcore/mcp.json',
125+
before: mcpSpec,
126+
after: afterMcpSpec,
127+
});
128+
}
129+
}
130+
87131
return { summary, directoriesToDelete: [], schemaChanges };
88132
}
89133

src/cli/tui/hooks/useRemove.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import {
1616
import { useCallback, useEffect, useRef, useState } from 'react';
1717

1818
// Re-export types for consumers
19-
export type { RemovableMemory, RemovableCredential as RemovableIdentity, RemovableGatewayTarget, RemovablePolicyResource };
19+
export type {
20+
RemovableMemory,
21+
RemovableCredential as RemovableIdentity,
22+
RemovableGatewayTarget,
23+
RemovablePolicyResource,
24+
};
2025

2126
// ============================================================================
2227
// Generic Hooks

src/cli/tui/screens/mcp/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
ApiGatewayHttpMethod,
33
GatewayAuthorizerType,
44
GatewayExceptionLevel,
5+
GatewayPolicyEngineConfiguration,
56
GatewayTargetType,
67
NodeRuntime,
78
PythonRuntime,
@@ -36,6 +37,8 @@ export interface AddGatewayConfig {
3637
enableSemanticSearch: boolean;
3738
/** Exception verbosity level for the gateway */
3839
exceptionLevel: GatewayExceptionLevel;
40+
/** Policy engine configuration for Cedar-based authorization */
41+
policyEngineConfiguration?: GatewayPolicyEngineConfiguration;
3942
}
4043

4144
/** Item ID for the semantic search toggle in the advanced config pane. */

src/cli/tui/screens/remove/RemoveFlow.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,22 @@ export function RemoveFlow({
111111
const { tools: mcpTools, isLoading: isLoadingTools, refresh: refreshTools } = useRemovableGatewayTargets();
112112
const { memories, isLoading: isLoadingMemories, refresh: refreshMemories } = useRemovableMemories();
113113
const { identities, isLoading: isLoadingIdentities, refresh: refreshIdentities } = useRemovableIdentities();
114-
const { policyEngines, isLoading: isLoadingPolicyEngines, refresh: refreshPolicyEngines } = useRemovablePolicyEngines();
114+
const {
115+
policyEngines,
116+
isLoading: isLoadingPolicyEngines,
117+
refresh: refreshPolicyEngines,
118+
} = useRemovablePolicyEngines();
115119
const { policies, isLoading: isLoadingPolicies, refresh: refreshPolicies } = useRemovablePolicies();
116120

117121
// Check if any data is still loading
118-
const isLoading = isLoadingAgents || isLoadingGateways || isLoadingTools || isLoadingMemories || isLoadingIdentities || isLoadingPolicyEngines || isLoadingPolicies;
122+
const isLoading =
123+
isLoadingAgents ||
124+
isLoadingGateways ||
125+
isLoadingTools ||
126+
isLoadingMemories ||
127+
isLoadingIdentities ||
128+
isLoadingPolicyEngines ||
129+
isLoadingPolicies;
119130

120131
// Preview hook
121132
const {
@@ -157,7 +168,15 @@ export function RemoveFlow({
157168
// In non-interactive mode, exit after success
158169
useEffect(() => {
159170
if (!isInteractive) {
160-
const successStates = ['agent-success', 'gateway-success', 'tool-success', 'memory-success', 'identity-success', 'policy-engine-success', 'policy-success'];
171+
const successStates = [
172+
'agent-success',
173+
'gateway-success',
174+
'tool-success',
175+
'memory-success',
176+
'identity-success',
177+
'policy-engine-success',
178+
'policy-success',
179+
];
161180
if (successStates.includes(flow.name)) {
162181
onExit();
163182
}
@@ -335,7 +354,9 @@ export function RemoveFlow({
335354
async (compositeKey: string) => {
336355
const result = await loadPolicyPreview(compositeKey);
337356
if (result.ok) {
338-
const policyName = compositeKey.includes('/') ? compositeKey.slice(compositeKey.indexOf('/') + 1) : compositeKey;
357+
const policyName = compositeKey.includes('/')
358+
? compositeKey.slice(compositeKey.indexOf('/') + 1)
359+
: compositeKey;
339360
if (force) {
340361
setFlow({ name: 'loading', message: `Removing policy ${policyName}...` });
341362
const removeResult = await removePolicyOp(compositeKey, result.preview);
@@ -532,8 +553,24 @@ export function RemoveFlow({
532553
]);
533554

534555
const refreshAll = useCallback(async () => {
535-
await Promise.all([refreshAgents(), refreshGateways(), refreshTools(), refreshMemories(), refreshIdentities(), refreshPolicyEngines(), refreshPolicies()]);
536-
}, [refreshAgents, refreshGateways, refreshTools, refreshMemories, refreshIdentities, refreshPolicyEngines, refreshPolicies]);
556+
await Promise.all([
557+
refreshAgents(),
558+
refreshGateways(),
559+
refreshTools(),
560+
refreshMemories(),
561+
refreshIdentities(),
562+
refreshPolicyEngines(),
563+
refreshPolicies(),
564+
]);
565+
}, [
566+
refreshAgents,
567+
refreshGateways,
568+
refreshTools,
569+
refreshMemories,
570+
refreshIdentities,
571+
refreshPolicyEngines,
572+
refreshPolicies,
573+
]);
537574

538575
// Select screen - wait for data to load to avoid arrow position issues
539576
if (flow.name === 'select') {

src/cli/tui/screens/remove/RemovePolicyScreen.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { RemovablePolicyResource } from '../../hooks/useRemove';
21
import { SelectScreen } from '../../components';
2+
import type { RemovablePolicyResource } from '../../hooks/useRemove';
33
import React from 'react';
44

55
interface RemovePolicyScreenProps {
@@ -22,11 +22,6 @@ export function RemovePolicyScreen({ policies, onSelect, onExit }: RemovePolicyS
2222
});
2323

2424
return (
25-
<SelectScreen
26-
title="Select Policy to Remove"
27-
items={items}
28-
onSelect={item => onSelect(item.id)}
29-
onExit={onExit}
30-
/>
25+
<SelectScreen title="Select Policy to Remove" items={items} onSelect={item => onSelect(item.id)} onExit={onExit} />
3126
);
3227
}

src/cli/tui/screens/remove/useRemoveFlow.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowS
7878
items.push(`${projectSpec.credentials.length} credential${projectSpec.credentials.length > 1 ? 's' : ''}`);
7979
}
8080
if (projectSpec.policyEngines && projectSpec.policyEngines.length > 0) {
81-
items.push(`${projectSpec.policyEngines.length} policy engine${projectSpec.policyEngines.length > 1 ? 's' : ''}`);
81+
items.push(
82+
`${projectSpec.policyEngines.length} policy engine${projectSpec.policyEngines.length > 1 ? 's' : ''}`
83+
);
8284
const totalPolicies = projectSpec.policyEngines.reduce((sum, e) => sum + (e.policies?.length ?? 0), 0);
8385
if (totalPolicies > 0) {
8486
items.push(`${totalPolicies} polic${totalPolicies > 1 ? 'ies' : 'y'}`);

src/schema/schemas/mcp.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,21 @@ export type AgentCoreGatewayTarget = z.infer<typeof AgentCoreGatewayTargetSchema
593593
export const GatewayExceptionLevelSchema = z.enum(['NONE', 'DEBUG']);
594594
export type GatewayExceptionLevel = z.infer<typeof GatewayExceptionLevelSchema>;
595595

596+
// ============================================================================
597+
// Gateway Policy Engine Configuration
598+
// ============================================================================
599+
600+
export const PolicyEngineModeSchema = z.enum(['LOG_ONLY', 'ENFORCE']);
601+
export type PolicyEngineMode = z.infer<typeof PolicyEngineModeSchema>;
602+
603+
export const GatewayPolicyEngineConfigurationSchema = z
604+
.object({
605+
policyEngineName: z.string().min(1),
606+
mode: PolicyEngineModeSchema,
607+
})
608+
.strict();
609+
export type GatewayPolicyEngineConfiguration = z.infer<typeof GatewayPolicyEngineConfigurationSchema>;
610+
596611
// ============================================================================
597612
// Gateway
598613
// ============================================================================
@@ -614,6 +629,8 @@ export const AgentCoreGatewaySchema = z
614629
enableSemanticSearch: z.boolean().default(true),
615630
/** Exception verbosity level. 'NONE' = generic errors (default), 'DEBUG' = verbose errors. */
616631
exceptionLevel: GatewayExceptionLevelSchema.default('NONE'),
632+
/** Policy engine configuration for Cedar-based authorization of tool calls. */
633+
policyEngineConfiguration: GatewayPolicyEngineConfigurationSchema.optional(),
617634
})
618635
.strict()
619636
.refine(

0 commit comments

Comments
 (0)