From a5e21de6171831474779063996faddb88a60ddd9 Mon Sep 17 00:00:00 2001 From: znerol2 <6800371+znerol2@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:10:01 +0100 Subject: [PATCH 1/4] feat: gate configure on setup receipt Signed-off-by: znerol2 <6800371+znerol2@users.noreply.github.com> --- frontend/src/App.tsx | 14 +++++-- frontend/src/create/ConnectStep.tsx | 46 ++++++++++++++++++-- frontend/src/hooks/useCreateFlow.ts | 10 +++-- src/dmf_init/bootstrap_steps.py | 49 ++++++++++++++++++---- src/dmf_init/main.py | 32 ++++++++++++++ src/dmf_init/orchestrate.py | 32 +++++++++++++- tests/test_bootstrap_steps.py | 45 ++++++++++++++++++-- tests/test_orchestrate.py | 65 +++++++++++++++++++++++++++++ tests/test_retry.py | 50 +++++++++++++++++++--- 9 files changed, 315 insertions(+), 28 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9baff67..f778c5e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -207,13 +207,20 @@ export default function App() { const handleResumePause = useCallback(async () => { if (!activePause || !createState.runId) return + if (activePause.pause_id === 'setup-request') return await resumePause(createState.runId, activePause.pause_id) }, [activePause, createState.runId, resumePause]) + const handleSetupRequestResume = useCallback(async (requestId: string) => { + if (!createState.runId) return + await resumePause(createState.runId, 'setup-request', { request_id: requestId }) + }, [createState.runId, resumePause]) + const handleVerifyPasskey = useCallback(async () => { if (!createState.runId) return - await verifyPasskey(createState.runId) - }, [createState.runId, verifyPasskey]) + if (activePause?.pause_id !== 'passkey' && activePause?.pause_id !== 'initial-passkey') return + await verifyPasskey(createState.runId, activePause.pause_id) + }, [activePause, createState.runId, verifyPasskey]) // Auto-start bootstrap exactly ONCE per deploy. The ref latch is essential: // guarding on !runId alone re-fired the effect after every reducer reset and @@ -313,6 +320,7 @@ export default function App() { passkeyChecking={createFlow.passkeyChecking} passkeyStatus={createFlow.passkeyStatus} onResume={handleResumePause} + onSetupRequestResume={handleSetupRequestResume} onVerifyPasskey={handleVerifyPasskey} envId={renderedEnvId ?? ''} runId={createState.runId ?? ''} @@ -343,7 +351,7 @@ export default function App() { } // Sidebar sub-items for the active rail phase. - const PAUSE_IDS = ['workstation', 'passkey'] + const PAUSE_IDS = ['workstation', 'passkey', 'initial-passkey', 'setup-request'] const wizardPages = ['Identity', 'Target node', 'Security', 'Review & Deploy'] const subItems: Record = { configure: wizardPages.map((label, i) => ({ diff --git a/frontend/src/create/ConnectStep.tsx b/frontend/src/create/ConnectStep.tsx index 98ef67a..dc925e0 100644 --- a/frontend/src/create/ConnectStep.tsx +++ b/frontend/src/create/ConnectStep.tsx @@ -35,6 +35,7 @@ type ConnectStepProps = { passkeyChecking: boolean passkeyStatus: string | null onResume: () => void + onSetupRequestResume: (requestId: string) => void onVerifyPasskey: () => void envId: string runId: string @@ -49,12 +50,14 @@ export function ConnectStep({ passkeyChecking, passkeyStatus, onResume, + onSetupRequestResume, onVerifyPasskey, runId, pollPasskey, bundleSafe, }: ConnectStepProps) { const [passkeyCount, setPasskeyCount] = useState<{ confirmed: number; required: number } | null>(null) + const [setupRequestId, setSetupRequestId] = useState('') const stationRef = useRef(null) useEffect(() => { @@ -64,7 +67,8 @@ export function ConnectStep({ }, [activePause?.pause_id]) useEffect(() => { - if (activePause?.pause_id !== 'passkey' || !runId) return + if (activePause?.pause_id !== 'passkey' && activePause?.pause_id !== 'initial-passkey') return + if (!runId) return let cancelled = false const interval = setInterval(async () => { const result = await pollPasskey(runId) @@ -76,6 +80,10 @@ export function ConnectStep({ return () => { cancelled = true; clearInterval(interval) } }, [activePause?.pause_id, runId, pollPasskey]) + useEffect(() => { + setSetupRequestId('') + }, [activePause?.pause_id]) + function downloadCaCertificate(payload: CaCertPayload) { const blob = new Blob([payload.pem], { type: 'application/x-pem-file' }) const objectUrl = URL.createObjectURL(blob) @@ -165,7 +173,7 @@ export function ConnectStep({ )} {/* Passkey station */} - {activePause.pause_id === 'passkey' && ( + {(activePause.pause_id === 'passkey' || activePause.pause_id === 'initial-passkey') && ( )} + {activePause.pause_id === 'setup-request' && ( +
+

+ {(activePause.payload.message as string) ?? + 'Create the fixed born-inventory setup request in the DMF Console, then paste its request id here.'} +

+ +
+ )} + {/* Export-gate station: block resume until recovery bundle is saved */} {activePause.pause_id === 'checkpoint-2-export-gate' && (
@@ -200,7 +229,7 @@ export function ConnectStep({ {/* Continue button */}
- {activePause.pause_id === 'passkey' ? ( + {activePause.pause_id === 'passkey' || activePause.pause_id === 'initial-passkey' ? ( + ) : activePause.pause_id === 'setup-request' ? ( + ) : (