Wizard
Session-based, multi-step configuration wizard framework driven through gateway RPC.
The wizard module is a session-based wizard framework exposed as wizard.* JSON-RPC methods on the gateway WebSocket. Every interaction is RPC-driven — there is no aleph or aleph-server CLI subcommand for it.
Design Philosophy
- Session-based state machine —
session_idkeeps progress across multiplewizard.next/wizard.answercalls RpcPrompterabstraction — the flow implementation holds anRpcPrompter, pushesWizardStepto the client, waits forwizard.answerto deliver the value- Panel / desktop / any RPC client can drive it; no terminal required
- Sticky terminal states —
cancel()afterDonedoes not overwrite the completed state or discard itsfinish_datapayload
Architecture
Client (Panel/desktop) WizardSession WizardFlow (background task)
│ │ │
│── wizard.start ────────────────────▶│ │
│ { wizard_type:"onboarding" } │── new(flow) tokio::spawn ─────▶│
│◀── { session_id, step:welcome } ───│ │
│ │◀── prompter.prompt(welcome) ───│
│ │ │
│── wizard.answer ──────────────────▶│ │
│ { session_id, step_id, value } │── answer(step_id,value) ──────▶│
│ │◀── prompter.prompt(next) ──────│
│◀── { step:primary } ───────────────│ │
│ │ │
│── wizard.cancel ──────────────────▶│ │
│ { session_id } │── cancel_tx.send(()) ─────────▶│
│◀── { cancelled:true } ─────────────│◀── settle(Cancelled) ──────────│Core Components
WizardSession
A session is created by wizard.start and runs the flow in a background tokio task; the client advances by calling wizard.next / wizard.answer:
pub struct WizardSession {
id: String,
status: Arc<RwLock<WizardStatus>>,
current_step: Arc<RwLock<Option<WizardStep>>>,
step_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<WizardStep>>>,
answers: Arc<RwLock<HashMap<String, PendingAnswer>>>,
error: Arc<RwLock<Option<String>>>,
finish_data: Arc<RwLock<Option<serde_json::Value>>>,
cancel_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
}Key properties:
step_txis owned only by theRpcPrompterinside the spawned flow task. When the task ends the prompter is dropped, the last sender goes away, the channel closes, andnext()receivesNone— that is how it surfacesDone/Error.- Terminal states are sticky.
settle()only accepts transitions out ofRunning; a latecancel()(e.g. client-disconnect cleanup in the result-pending window) cannot clobber a completedDoneand discard itsfinish_data.
WizardFlow trait
#[async_trait]
pub trait WizardFlow: Send + Sync {
async fn run(&self, prompter: &RpcPrompter) -> Result<(), WizardSessionError>;
fn name(&self) -> &str { "wizard" }
}RpcPrompter
The only shipped prompter. prompter.prompt(step) pushes a WizardStep to the client and blocks until the matching wizard.answer arrives. finish(data) writes the terminal payload into finish_data, and the next wizard.next returns it as WizardNextResult.data on a Done.
Supported step types:
StepType | Client interaction |
|---|---|
Note | Display only |
Select | Single select |
MultiSelect | Multi-select |
Text | Free text (with sensitive / validation) |
Confirm | Yes/no (with default) |
Progress | Background progress (StepExecutor::Gateway, client just renders) |
Action | Server-executed action |
Built-in Flows
| Flow | wizard_type | Purpose |
|---|---|---|
OnboardingFlow | onboarding | First-launch: provider × 2, model, thinking, messaging, review (6 steps) |
QuickSetupFlow | quick-setup | One API key, skip the rest (re-install / quick path) |
ProviderSetupFlow | provider-setup | Add / reconfigure a single provider |
OnboardingFlow::run step sequence:
welcome— introconfigure_primary— pick primary provider + model + API keyconfigure_secondary— pick secondary provider + model + API keyconfigure_thinking— thinking level (low/medium/high)configure_messaging— Telegram / Discord / iMessage (Slack is currently disabled)review_and_finalize— review and persist
None of these are triggered from the CLI; the Panel settings page (or the post-install handshake window) calls wizard.start directly.
RPC Protocol
Methods registered in the gateway handler set. service_unavailable placeholders are replaced with the real handlers via install_wizard_handlers once the boot path initialises the WizardSessionManager:
| Method | Params | Returns |
|---|---|---|
wizard.start | { wizard_type, initial_data? } | { session_id, step, status } |
wizard.next | { session_id, answer? } | WizardNextResult (next step / done / cancelled / error) |
wizard.answer | { session_id, step_id, value } | WizardNextResult |
wizard.cancel | { session_id } | { cancelled } |
wizard.status | { session_id } | { status, error?, current_step? } |
The terminal WizardNextResult can carry a data payload that the flow sets via RpcPrompter::finish(data); onboarding typically uses this to return the issued token or a config-write summary.
Safety / Concurrency
AtomicU64generates step IDs (step-N), lock-freeunwrap_or_else(|e| e.into_inner())lock-recovery pattern (poisoned-lock safe)- Explicit step validation —
answerchecks thatstep_idmatches the current step; carrying anansweronwizard.nextis rejected (wizard.answeris the only path that accepts values) - Channel-leak guard —
RpcPrompter::promptremoves the pending sender from theanswersmap if the step push fails, so it cannot leak for the rest of the session - No in-process
CliPrompter— terminal UIs go through RPC and are rendered by Panel; the CLI never drives the wizard directly
Code Locations
src/wizard/mod.rs— module entry pointsrc/wizard/session.rs—WizardSession, sticky terminal states, channel lifecyclesrc/wizard/prompter.rs—RpcPrompter,PendingAnswer,ProgressHandlesrc/wizard/types.rs—WizardStep/WizardOption/WizardStatus/StepType/WizardNextResult/WizardAnswersrc/wizard/flows/onboarding.rs—OnboardingFlow/QuickSetupFlow/ProviderSetupFlowsrc/gateway/handlers/wizard.rs—wizard.*RPC handlers,WizardSessionManager,install_wizard_handlers
Related Pages
- Setup Wizard — User-facing onboarding flow
- Gateway RPC
wizard.*— Protocol detail - Deployment — Post-install handshake window