Skip to content

Commit c576d02

Browse files
fix: defer policy engine write and harden policy flow UX (#856)
* fix: defer policy engine write to disk until flow completes Previously, pressing Escape on the gateway selection screen during policy engine creation would skip to the success screen because the engine was already written to agentcore.json at the name step. Now the disk write is deferred until the user completes the entire flow, so Escape correctly navigates back to the previous step without persisting a half-configured engine. Constraint: Must not break non-interactive CLI path which still writes immediately via primitive Rejected: Only change Escape to go back without deferring write | engine would still be persisted on back Confidence: high Scope-risk: narrow * fix: preserve engine name when navigating back from gateway selection When pressing Escape on the gateway screen to go back to the name step, the previously entered engine name was lost because AddPolicyEngineScreen remounted with a generated default. Now the entered name is stored in pendingEngineName state and passed back as initialName so the user sees their original input. Constraint: Must not change flow state union type to keep diff minimal Rejected: Carry name in FlowState union variant | adds complexity to type for one field Confidence: high Scope-risk: narrow * chore: remove TUI harness test accidentally committed This test requires a live terminal session and cannot run as a unit test in CI. It was an untracked local file that got staged by mistake.
1 parent 6cc575c commit c576d02

2 files changed

Lines changed: 38 additions & 28 deletions

File tree

src/cli/tui/screens/policy/AddPolicyEngineScreen.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ interface AddPolicyEngineScreenProps {
99
onComplete: (config: AddPolicyEngineConfig) => void;
1010
onExit: () => void;
1111
existingEngineNames: string[];
12+
initialName?: string;
1213
headerContent?: React.ReactNode;
1314
}
1415

1516
export function AddPolicyEngineScreen({
1617
onComplete,
1718
onExit,
1819
existingEngineNames,
20+
initialName,
1921
headerContent,
2022
}: AddPolicyEngineScreenProps) {
2123
return (
@@ -24,7 +26,7 @@ export function AddPolicyEngineScreen({
2426
<TextInput
2527
key="name"
2628
prompt="Policy engine name"
27-
initialValue={generateUniqueName('MyPolicyEngine', existingEngineNames)}
29+
initialValue={initialName ?? generateUniqueName('MyPolicyEngine', existingEngineNames)}
2830
onSubmit={name => onComplete({ name })}
2931
onCancel={onExit}
3032
schema={PolicyEngineNameSchema}

src/cli/tui/screens/policy/AddPolicyFlow.tsx

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
5656
const [engineNames, setEngineNames] = useState<string[]>([]);
5757
const [policyNames, setPolicyNames] = useState<string[]>([]);
5858
const [hasUnprotectedGateways, setHasUnprotectedGateways] = useState(false);
59+
const [pendingEngineName, setPendingEngineName] = useState<string | undefined>();
5960

6061
const engineSteps = useMemo<EngineCreationStep[]>(() => {
6162
const steps: EngineCreationStep[] = ['name'];
@@ -126,23 +127,32 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
126127
}
127128
}, []);
128129

129-
const handleEngineComplete = useCallback(async (config: AddPolicyEngineConfig) => {
130-
const result = await policyEnginePrimitive.add({
131-
name: config.name,
132-
});
130+
const commitEngine = useCallback(async (engineName: string, gateways?: string[], mode?: 'LOG_ONLY' | 'ENFORCE') => {
131+
const result = await policyEnginePrimitive.add({ name: engineName });
132+
if (!result.success) {
133+
setFlow({ name: 'error', message: result.error });
134+
return;
135+
}
136+
setEngineNames(prev => [...prev, engineName]);
137+
setPendingEngineName(undefined);
138+
if (gateways && gateways.length > 0 && mode) {
139+
await policyEnginePrimitive.attachToGateways(engineName, gateways, mode);
140+
}
141+
setFlow({ name: 'engine-success', engineName });
142+
}, []);
133143

134-
if (result.success) {
135-
setEngineNames(prev => [...prev, config.name]);
144+
const handleEngineComplete = useCallback(
145+
async (config: AddPolicyEngineConfig) => {
146+
setPendingEngineName(config.name);
136147
const unprotected = await policyEnginePrimitive.getUnprotectedGateways();
137148
if (unprotected.length > 0) {
138149
setFlow({ name: 'attach-gateways', engineName: config.name, gateways: unprotected });
139150
} else {
140-
setFlow({ name: 'engine-success', engineName: config.name });
151+
void commitEngine(config.name);
141152
}
142-
} else {
143-
setFlow({ name: 'error', message: result.error });
144-
}
145-
}, []);
153+
},
154+
[commitEngine]
155+
);
146156

147157
const handlePolicyComplete = useCallback(async (config: AddPolicyConfig) => {
148158
const result = await policyPrimitive.add({
@@ -201,6 +211,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
201211
return (
202212
<AddPolicyEngineScreen
203213
existingEngineNames={engineNames}
214+
initialName={pendingEngineName}
204215
headerContent={<StepIndicator steps={engineSteps} currentStep="name" labels={ENGINE_STEP_LABELS} />}
205216
onComplete={(config: AddPolicyEngineConfig) => void handleEngineComplete(config)}
206217
onExit={() => {
@@ -238,7 +249,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
238249
stepIndicator={<StepIndicator steps={engineSteps} currentStep="attach-gateways" labels={ENGINE_STEP_LABELS} />}
239250
onConfirm={selected => {
240251
if (selected.length === 0) {
241-
setFlow({ name: 'engine-success', engineName: flow.engineName });
252+
void commitEngine(flow.engineName);
242253
} else {
243254
setFlow({
244255
name: 'attach-mode',
@@ -248,7 +259,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
248259
});
249260
}
250261
}}
251-
onSkip={() => setFlow({ name: 'engine-success', engineName: flow.engineName })}
262+
onBack={() => setFlow({ name: 'engine-wizard' })}
252263
/>
253264
);
254265
}
@@ -261,15 +272,12 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD
261272
gatewayCount={flow.selectedGateways.length}
262273
stepIndicator={<StepIndicator steps={engineSteps} currentStep="attach-mode" labels={ENGINE_STEP_LABELS} />}
263274
onSelect={mode => {
264-
void policyEnginePrimitive
265-
.attachToGateways(flow.engineName, flow.selectedGateways, mode)
266-
.then(() => setFlow({ name: 'engine-success', engineName: flow.engineName }))
267-
.catch(err =>
268-
setFlow({
269-
name: 'error',
270-
message: err instanceof Error ? err.message : 'Failed to attach policy engine',
271-
})
272-
);
275+
void commitEngine(flow.engineName, flow.selectedGateways, mode).catch(err =>
276+
setFlow({
277+
name: 'error',
278+
message: err instanceof Error ? err.message : 'Failed to attach policy engine',
279+
})
280+
);
273281
}}
274282
onBack={() => {
275283
setFlow({ name: 'attach-gateways', engineName: flow.engineName, gateways: flow.allGateways });
@@ -360,13 +368,13 @@ function AttachGatewaysScreen({
360368
engineName,
361369
gateways,
362370
onConfirm,
363-
onSkip,
371+
onBack,
364372
stepIndicator,
365373
}: {
366374
engineName: string;
367375
gateways: string[];
368376
onConfirm: (selected: string[]) => void;
369-
onSkip: () => void;
377+
onBack: () => void;
370378
stepIndicator?: React.ReactNode;
371379
}) {
372380
const items: SelectableItem[] = useMemo(() => gateways.map(name => ({ id: name, title: name })), [gateways]);
@@ -375,16 +383,16 @@ function AttachGatewaysScreen({
375383
items,
376384
getId: item => item.id,
377385
onConfirm: ids => onConfirm([...ids]),
378-
onExit: onSkip,
386+
onExit: onBack,
379387
isActive: true,
380388
requireSelection: false,
381389
});
382390

383391
return (
384392
<Screen
385393
title="Attach Policy Engine"
386-
onExit={onSkip}
387-
helpText="Space toggle · Enter confirm · Esc skip · Ctrl+C quit"
394+
onExit={onBack}
395+
helpText="Space toggle · Enter confirm · Esc back · Ctrl+C quit"
388396
headerContent={stepIndicator}
389397
>
390398
<Panel>

0 commit comments

Comments
 (0)