provisioning/docs/src/architecture/adr/adr-013-typdialog-integration.md

589 lines
19 KiB
Markdown
Raw Normal View History

2026-01-08 21:22:57 +00:00
# ADR-013: Typdialog Web UI Backend Integration for Interactive Configuration
## Status
**Accepted** - 2025-01-08
## Context
The provisioning system requires interactive user input for configuration workflows, workspace initialization, credential setup, and guided deployment scenarios. The system architecture combines Rust (performance-critical), Nushell (scripting), and Nickel (declarative configuration), creating challenges for interactive form-based input and multi-user collaboration.
### The Interactive Configuration Problem
**Current limitations**:
1. **Nushell CLI**: Terminal-only interaction
- `input` command: Single-line text prompts only
- No form validation, no complex multi-field forms
- Limited to single-user, terminal-bound workflows
- User experience: Basic and error-prone
2. **Nickel**: Declarative configuration language
- Cannot handle interactive prompts (by design)
- Pure evaluation model (no side effects)
- Forms must be defined statically, not interactively
- No runtime user interaction
3. **Existing Solutions**: Inadequate for modern infrastructure provisioning
- **Shell-based prompts**: Error-prone, no validation, single-user
- **Custom web forms**: High maintenance, inconsistent UX
- **Separate admin panels**: Disconnected from IaC workflow
- **Terminal-only TUI**: Limited to SSH sessions, no collaboration
### Use Cases Requiring Interactive Input
1. **Workspace Initialization**:
```nushell
# Current: Error-prone prompts
let workspace_name = input "Workspace name: "
let provider = input "Provider (aws/azure/oci): "
# No validation, no autocomplete, no guidance
```
2. **Credential Setup**:
```nushell
# Current: Insecure and basic
let api_key = input "API Key: " # Shows in terminal history
let region = input "Region: " # No validation
```
3. **Configuration Wizards**:
- Database connection setup (host, port, credentials, SSL)
- Network configuration (CIDR blocks, subnets, gateways)
- Security policies (encryption, access control, audit)
4. **Guided Deployments**:
- Multi-step infrastructure provisioning
- Service selection with dependencies
- Environment-specific overrides
### Requirements for Interactive Input System
-**Terminal UI widgets**: Text input, password, select, multi-select, confirm
-**Validation**: Type checking, regex patterns, custom validators
-**Security**: Password masking, sensitive data handling
-**User Experience**: Arrow key navigation, autocomplete, help text
-**Composability**: Chain multiple prompts into forms
-**Error Handling**: Clear validation errors, retry logic
-**Rust Integration**: Native Rust library (no subprocess overhead)
-**Cross-Platform**: Works on Linux, macOS, Windows
## Decision
Integrate **typdialog** with its **Web UI backend** as the standard interactive configuration interface for the provisioning platform. The major achievement of typdialog is not the TUI - it is the Web UI backend that enables browser-based forms, multi-user collaboration, and seamless integration with the provisioning orchestrator.
### Architecture Diagram
```
┌─────────────────────────────────────────┐
│ Nushell Script │
│ │
│ provisioning workspace init │
│ provisioning config setup │
│ provisioning deploy guided │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Rust CLI Handler │
│ (provisioning/core/cli/) │
│ │
│ - Parse command │
│ - Determine if interactive needed │
│ - Invoke TUI dialog module │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ TUI Dialog Module │
│ (typdialog wrapper) │
│ │
│ - Form definition (validation rules) │
│ - Widget rendering (text, select) │
│ - User input capture │
│ - Validation execution │
│ - Result serialization (JSON/TOML) │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ typdialog Library │
│ │
│ - Terminal rendering (crossterm) │
│ - Event handling (keyboard, mouse) │
│ - Widget state management │
│ - Input validation engine │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Terminal (stdout/stdin) │
│ │
│ ✅ Rich TUI with validation │
│ ✅ Secure password input │
│ ✅ Guided multi-step forms │
└─────────────────────────────────────────┘
```
### Implementation Characteristics
**CLI Integration Provides**:
- ✅ Native Rust commands with TUI dialogs
- ✅ Form-based input for complex configurations
- ✅ Validation rules defined in Rust (type-safe)
- ✅ Secure input (password masking, no history)
- ✅ Error handling with retry logic
- ✅ Serialization to Nickel/TOML/JSON
**TUI Dialog Library Handles**:
- ✅ Terminal UI rendering and event loop
- ✅ Widget management (text, select, checkbox, confirm)
- ✅ Input validation and error display
- ✅ Navigation (arrow keys, tab, enter)
- ✅ Cross-platform terminal compatibility
## Rationale
### Why TUI Dialog Integration Is Required
| Aspect | Shell Prompts (current) | Web Forms | TUI Dialog (chosen) |
|--------|-------------------------|-----------|---------------------|
| **User Experience** | ❌ Basic text only | ✅ Rich UI | ✅ Rich TUI |
| **Validation** | ❌ Manual, error-prone | ✅ Built-in | ✅ Built-in |
| **Security** | ❌ Plain text, history | ⚠️ Network risk | ✅ Secure terminal |
| **Setup Complexity** | ✅ None | ❌ Server required | ✅ Minimal |
| **Terminal Workflow** | ✅ Native | ❌ Browser switch | ✅ Native |
| **Offline Support** | ✅ Always | ❌ Requires server | ✅ Always |
| **Dependencies** | ✅ None | ❌ Web stack | ✅ Single crate |
| **Error Handling** | ❌ Manual | ⚠️ Complex | ✅ Built-in retry |
### The Nushell Limitation
Nushell's `input` command is limited:
```nushell
# Current: No validation, no security
let password = input "Password: " # ❌ Shows in terminal
let region = input "AWS Region: " # ❌ No autocomplete/validation
# Cannot do:
# - Multi-select from options
# - Conditional fields (if X then ask Y)
# - Password masking
# - Real-time validation
# - Autocomplete/fuzzy search
```
### The Nickel Constraint
Nickel is declarative and cannot prompt users:
```nickel
# Nickel defines what the config looks like, NOT how to get it
{
database = {
host | String,
port | Number,
credentials | { username: String, password: String },
}
}
# Nickel cannot:
# - Prompt user for values
# - Show interactive forms
# - Validate input interactively
```
### Why Rust + TUI Dialog Is The Solution
**Rust provides**:
- Native terminal control (crossterm, termion)
- Type-safe form definitions
- Validation rules as functions
- Secure memory handling (password zeroization)
- Performance (no subprocess overhead)
**TUI Dialog provides**:
- Widget library (text, select, multi-select, confirm)
- Event loop and rendering
- Validation framework
- Error display and retry logic
**Integration enables**:
- Nushell calls Rust CLI → Shows TUI dialog → Returns validated config
- Nickel receives validated config → Type checks → Merges with defaults
## Consequences
### Positive
- **User Experience**: Professional TUI with validation and guidance
- **Security**: Password masking, sensitive data protection, no terminal history
- **Validation**: Type-safe rules enforced before config generation
- **Developer Experience**: Reusable form components across CLI commands
- **Error Handling**: Clear validation errors with retry options
- **Offline First**: No network dependencies for interactive input
- **Terminal Native**: Fits CLI workflow, no context switching
- **Maintainability**: Single library for all interactive input
### Negative
- **Terminal Dependency**: Requires interactive terminal (not scriptable)
- **Learning Curve**: Developers must learn TUI dialog patterns
- **Library Lock-in**: Tied to specific TUI library API
- **Testing Complexity**: Interactive tests require terminal mocking
- **Non-Interactive Fallback**: Need alternative for CI/CD and scripts
### Mitigation Strategies
**Non-Interactive Mode**:
```rust
// Support both interactive and non-interactive
if terminal::is_interactive() {
// Show TUI dialog
let config = show_workspace_form()?;
} else {
// Use config file or CLI args
let config = load_config_from_file(args.config)?;
}
```
**Testing**:
```rust
// Unit tests: Test form validation logic (no TUI)
#[test]
fn test_validate_workspace_name() {
assert!(validate_name("my-workspace").is_ok());
assert!(validate_name("invalid name!").is_err());
}
// Integration tests: Use mock terminal or config files
```
**Scriptability**:
```bash
# Batch mode: Provide config via file
provisioning workspace init --config workspace.toml
# Interactive mode: Show TUI dialog
provisioning workspace init --interactive
```
**Documentation**:
- Form schemas documented in `docs/`
- Config file examples provided
- Screenshots of TUI forms in guides
## Alternatives Considered
### Alternative 1: Shell-Based Prompts (Current State)
**Pros**: Simple, no dependencies
**Cons**: No validation, poor UX, security risks
**Decision**: REJECTED - Inadequate for production use
### Alternative 2: Web-Based Forms
**Pros**: Rich UI, well-known patterns
**Cons**: Requires server, network dependency, context switch
**Decision**: REJECTED - Too complex for CLI tool
### Alternative 3: Custom TUI Per Use Case
**Pros**: Tailored to each need
**Cons**: High maintenance, code duplication, inconsistent UX
**Decision**: REJECTED - Not sustainable
### Alternative 4: External Form Tool (dialog, whiptail)
**Pros**: Mature, cross-platform
**Cons**: Subprocess overhead, limited validation, shell escaping issues
**Decision**: REJECTED - Poor Rust integration
### Alternative 5: Text-Based Config Files Only
**Pros**: Fully scriptable, no interactive complexity
**Cons**: Steep learning curve, no guidance for new users
**Decision**: REJECTED - Poor user onboarding experience
## Implementation Details
### Form Definition Pattern
```rust
use typdialog::Form;
pub fn workspace_initialization_form() -> Result<WorkspaceConfig> {
let form = Form::new("Workspace Initialization")
.add_text_input("name", "Workspace Name")
.required()
.validator(|s| validate_workspace_name(s))
.add_select("provider", "Cloud Provider")
.options(&["aws", "azure", "oci", "local"])
.required()
.add_text_input("region", "Region")
.default("us-west-2")
.validator(|s| validate_region(s))
.add_password("admin_password", "Admin Password")
.required()
.min_length(12)
.add_confirm("enable_monitoring", "Enable Monitoring?")
.default(true);
let responses = form.run()?;
// Convert to strongly-typed config
let config = WorkspaceConfig {
name: responses.get_string("name")?,
provider: responses.get_string("provider")?.parse()?,
region: responses.get_string("region")?,
admin_password: responses.get_password("admin_password")?,
enable_monitoring: responses.get_bool("enable_monitoring")?,
};
Ok(config)
}
```
### Integration with Nickel
```rust
// 1. Get validated input from TUI dialog
let config = workspace_initialization_form()?;
// 2. Serialize to TOML/JSON
let config_toml = toml::to_string(&config)?;
// 3. Write to workspace config
fs::write("workspace/config.toml", config_toml)?;
// 4. Nickel merges with defaults
// nickel export workspace/main.ncl --format json
// (uses workspace/config.toml as input)
```
### CLI Command Structure
```rust
// provisioning/core/cli/src/commands/workspace.rs
#[derive(Parser)]
pub enum WorkspaceCommand {
Init {
#[arg(long)]
interactive: bool,
#[arg(long)]
config: Option<PathBuf>,
},
}
pub fn handle_workspace_init(args: InitArgs) -> Result<()> {
if args.interactive || terminal::is_interactive() {
// Show TUI dialog
let config = workspace_initialization_form()?;
config.save("workspace/config.toml")?;
} else if let Some(config_path) = args.config {
// Use provided config
let config = WorkspaceConfig::load(config_path)?;
config.save("workspace/config.toml")?;
} else {
bail!("Either --interactive or --config required");
}
// Continue with workspace setup
Ok(())
}
```
### Validation Rules
```rust
pub fn validate_workspace_name(name: &str) -> Result<(), String> {
// Alphanumeric, hyphens, 3-32 chars
let re = Regex::new(r"^[a-z0-9-]{3,32}$").unwrap();
if !re.is_match(name) {
return Err("Name must be 3-32 lowercase alphanumeric chars with hyphens".into());
}
Ok(())
}
pub fn validate_region(region: &str) -> Result<(), String> {
const VALID_REGIONS: &[&str] = &["us-west-1", "us-west-2", "us-east-1", "eu-west-1"];
if !VALID_REGIONS.contains(&region) {
return Err(format!("Invalid region. Must be one of: {}", VALID_REGIONS.join(", ")));
}
Ok(())
}
```
### Security: Password Handling
```rust
use zeroize::Zeroizing;
pub fn get_secure_password() -> Result<Zeroizing<String>> {
let form = Form::new("Secure Input")
.add_password("password", "Password")
.required()
.min_length(12)
.validator(password_strength_check);
let responses = form.run()?;
// Password automatically zeroized when dropped
let password = Zeroizing::new(responses.get_password("password")?);
Ok(password)
}
```
## Testing Strategy
**Unit Tests**:
```rust
#[test]
fn test_workspace_name_validation() {
assert!(validate_workspace_name("my-workspace").is_ok());
assert!(validate_workspace_name("UPPERCASE").is_err());
assert!(validate_workspace_name("ab").is_err()); // Too short
}
```
**Integration Tests**:
```rust
// Use non-interactive mode with config files
#[test]
fn test_workspace_init_non_interactive() {
let config = WorkspaceConfig {
name: "test-workspace".into(),
provider: Provider::Local,
region: "us-west-2".into(),
admin_password: "secure-password-123".into(),
enable_monitoring: true,
};
config.save("/tmp/test-config.toml").unwrap();
let result = handle_workspace_init(InitArgs {
interactive: false,
config: Some("/tmp/test-config.toml".into()),
});
assert!(result.is_ok());
}
```
**Manual Testing**:
```bash
# Test interactive flow
cargo build --release
./target/release/provisioning workspace init --interactive
# Test validation errors
# - Try invalid workspace name
# - Try weak password
# - Try invalid region
```
## Configuration Integration
**CLI Flag**:
```toml
# provisioning/config/config.defaults.toml
[ui]
interactive_mode = "auto" # "auto" | "always" | "never"
dialog_theme = "default" # "default" | "minimal" | "colorful"
```
**Environment Override**:
```bash
# Force non-interactive mode (for CI/CD)
export PROVISIONING_INTERACTIVE=false
# Force interactive mode
export PROVISIONING_INTERACTIVE=true
```
## Documentation Requirements
**User Guides**:
- `docs/user/interactive-configuration.md` - How to use TUI dialogs
- `docs/guides/workspace-setup.md` - Workspace initialization with screenshots
**Developer Documentation**:
- `docs/development/tui-forms.md` - Creating new TUI forms
- Form definition best practices
- Validation rule patterns
**Configuration Schema**:
```nickel
# provisioning/schemas/workspace.ncl
{
WorkspaceConfig = {
name
| doc "Workspace identifier (3-32 alphanumeric chars with hyphens)"
| String,
provider
| doc "Cloud provider"
| [| 'aws, 'azure, 'oci, 'local |],
region
| doc "Deployment region"
| String,
admin_password
| doc "Admin password (min 12 characters)"
| String,
enable_monitoring
| doc "Enable monitoring services"
| Bool,
}
}
```
## Migration Path
**Phase 1: Add Library**
- Add typdialog dependency to `provisioning/core/cli/Cargo.toml`
- Create TUI dialog wrapper module
- Implement basic text/select widgets
**Phase 2: Implement Forms**
- Workspace initialization form
- Credential setup form
- Configuration wizard forms
**Phase 3: CLI Integration**
- Update CLI commands to use TUI dialogs
- Add `--interactive` / `--config` flags
- Implement non-interactive fallback
**Phase 4: Documentation**
- User guides with screenshots
- Developer documentation for form creation
- Example configs for non-interactive use
**Phase 5: Testing**
- Unit tests for validation logic
- Integration tests with config files
- Manual testing on all platforms
## References
- [typdialog Crate](https://crates.io/crates/typdialog) (or similar: dialoguer, inquire)
- [crossterm](https://crates.io/crates/crossterm) - Terminal manipulation
- [zeroize](https://crates.io/crates/zeroize) - Secure memory zeroization
- ADR-004: Hybrid Architecture (Rust/Nushell integration)
- ADR-011: Nickel Migration (declarative config language)
- ADR-012: Nushell Plugins (CLI wrapper patterns)
- Nushell `input` command limitations: [Nushell Book - Input](https://www.nushell.sh/commands/docs/input.html)
---
**Status**: Accepted
**Last Updated**: 2025-01-08
**Implementation**: Planned
**Priority**: High (User onboarding and security)
**Estimated Complexity**: Moderate