* docs: design document for openhuman REPL / interactive shell (#92) Design-first writeup covering problem statement, UX sketch with example sessions (skills + non-skill flows), architecture showing how the REPL reuses existing invoke_method/controller registry/RuntimeEngine without duplicating logic, safety rules for secret redaction, and phased implementation milestones (MVP → skills commands → script/batch mode). Closes #92 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update dependencies and enhance onboarding logic - Updated Cargo.lock and Cargo.toml to include new dependencies: `clipboard-win`, `endian-type`, `error-code`, `fd-lock`, `home`, `nibble_vec`, `radix_trie`, `rustyline`, and `unicode-segmentation`. - Enhanced the OnboardingOverlay component to wait for user profile loading before checking onboarding status, improving user experience during onboarding. - Adjusted dependency versions and added features for `rustyline` in Cargo.toml. * feat(repl): implement interactive REPL for OpenHuman core - Added a new REPL module to provide an interactive shell for users, allowing command execution and evaluation. - Integrated REPL functionality into the CLI, enabling commands like `openhuman repl` and support for options such as `--eval` and `--batch`. - Enhanced command parsing and execution flow to maintain consistency with existing JSON-RPC server logic, ensuring no duplication of functionality. - Updated CLI help documentation to include REPL usage instructions. - Introduced Apple certificate import and code signing steps in the GitHub Actions workflow for macOS, enhancing sidecar security. This commit lays the groundwork for a more interactive user experience and strengthens the security of the sidecar binary. * refactor(repl): remove tool_warning_shown field from ReplState - Eliminated the `tool_warning_shown` boolean field from the `ReplState` struct, simplifying the state management within the REPL module. - Updated the constructor to reflect the removal of this field, ensuring consistency in the initialization of `ReplState`. * refactor(build): simplify Tauri build command in workflow - Removed unnecessary parameters from the Tauri build command in the GitHub Actions workflow, streamlining the build process. - Improved readability of the build configuration by eliminating redundant options. * chore(tauri): update build targets in tauri.conf.json - Changed the build targets from a single string to an array, specifying individual target formats for improved clarity and flexibility in the build process. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
Design: openhuman repl — Interactive Shell for Core Flows & Skills
Issue: tinyhumansai/openhuman#92 Status: Design (pre-implementation) Date: 2026-03-30
1. Problem Statement
Validating core behavior today requires either the full Tauri stack (UI + sidecar) or hand-crafted JSON-RPC / curl commands. Both are slow for iteration, especially for:
- Skill authors debugging QuickJS execution, tool wiring, and sandbox boundaries.
- Core developers exercising RPC controllers during development.
- QA / onboarding running reproducible smoke checks from a terminal.
A lightweight, terminal-first read-eval-print loop would make all three workflows significantly faster.
Target Users
| User | Need |
|---|---|
| Skill author | Discover, start, inspect, and call skill tools without a UI |
| Core developer | Exercise any registered RPC method, inspect config/health |
| QA / CI script | Batch-run a sequence of commands, assert outputs |
| New contributor | Follow onboarding docs ("paste this in the REPL to see X") |
Non-Goals (v1)
- Shipping to end users as a primary interface — this is a dev/test tool.
- Chat / LLM interaction — no agentic inference loop; use the desktop app.
- Remote connections — the REPL drives the local core directly (in-process), not a remote server.
- Full TUI (curses, panels, split panes) — keep it a single-line REPL with good completion.
- Plugin authoring within the REPL — skills are authored in JS files, not typed live.
- Replacing
openhuman run— the server subcommand stays as-is; the REPL is a peer.
2. Command / UX Sketch
Starting the REPL
openhuman repl # interactive mode
openhuman repl --verbose # debug logging enabled
openhuman repl --eval 'health snapshot' # evaluate one command, print, exit
echo 'config get' | openhuman repl --batch # stdin batch mode (no prompt)
Prompt
openhuman> _
Prompt changes to show context when relevant:
openhuman> skill start gmail
skill:gmail running (3 tools)
openhuman> _
Example Session: Listing and Invoking a Skill
openhuman> help
Commands:
<namespace> <function> [--param value ...] Call any registered controller
call <method> [json] Raw JSON-RPC method call
skill list List discovered skills
skill start <id> Start a skill instance
skill stop <id> Stop a running skill
skill status <id> Inspect runtime state
skill tools <id> List tools from a running skill
skill call <id> <tool> [json-args] Invoke a skill tool
schema [namespace] Show controller schemas
namespaces List all namespaces
env Show workspace & runtime paths
.verbose on|off Toggle debug logging
.json on|off Toggle raw JSON output
.time on|off Toggle timing display
exit | quit | Ctrl-D Exit
openhuman> skill list
ID NAME STATUS TOOLS
gmail Gmail pending -
notion Notion pending -
calendar Google Calendar pending -
openhuman> skill start gmail
skill:gmail initializing...
skill:gmail running (3 tools)
openhuman> skill tools gmail
TOOL DESCRIPTION
gmail__search_emails Search emails by query
gmail__send_email Send an email
gmail__get_thread Get full thread by ID
openhuman> skill call gmail search_emails {"query": "from:alice", "max_results": 5}
{
"content": [{ "type": "text", "text": "[...results...]" }],
"is_error": false
}
(234ms)
openhuman> skill stop gmail
skill:gmail stopped
Example Session: Non-Skill Flow (Config + Health)
openhuman> config get
{
"workspace_dir": "/Users/dev/.openhuman/workspace",
"model_settings": { "model_id": "neocortex-mk1", ... },
...
}
openhuman> health snapshot
{
"uptime_secs": 42,
"skills_running": 1,
...
}
openhuman> config get_runtime_flags
{
"browser_allow_all": false,
"local_ai_enabled": false,
...
}
Example: Raw JSON-RPC Style
openhuman> call openhuman.encrypt_secret {"plaintext": "my-api-key"}
{
"ciphertext": "enc:v1:..."
}
Example: Scriptable / Batch Mode
# One-liner for CI
openhuman repl --eval 'health snapshot' | jq '.uptime_secs'
# Batch script
cat <<'EOF' | openhuman repl --batch
config get_runtime_flags
skill list
health snapshot
EOF
3. Architecture
3.1 How the REPL Reuses Core Code
The REPL is not a second implementation. It drives the same code paths the JSON-RPC server uses:
┌────────────────────────────┐
│ openhuman repl │
│ (rustyline read loop) │
│ │
│ parse_line() │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Skill shorthand? │─yes──┼──► RuntimeEngine API (in-process)
│ │ (skill list, etc) │ │ engine.discover_skills()
│ └────────┬─────────┘ │ engine.start_skill()
│ │ no │ engine.call_tool()
│ ▼ │ engine.list_skills()
│ ┌──────────────────┐ │
│ │ Meta command? │─yes──┼──► .verbose, .json, env, help (local)
│ │ (.verbose, help) │ │
│ └────────┬─────────┘ │
│ │ no │
│ ▼ │
│ ┌──────────────────┐ │
│ │ namespace func │──────┼──► invoke_method(state, method, params)
│ │ or call <method> │ │ ↓ (same path as JSON-RPC server)
│ └──────────────────┘ │ all::try_invoke_registered_rpc()
│ │ → domain handler
└────────────────────────────┘
Key reuse points in existing code:
| What | Where | How REPL uses it |
|---|---|---|
| Controller registry | src/core/all.rs |
all_controller_schemas(), schema_for_rpc_method(), try_invoke_registered_rpc() |
| Method invocation | src/core/jsonrpc.rs |
invoke_method(state, method, params) — identical call to server |
| Param parsing & validation | src/core/cli.rs + all.rs |
parse_function_params(), validate_params() |
| Schema grouping / help | src/core/cli.rs |
grouped_schemas(), print_namespace_help() |
| Skills runtime | src/openhuman/skills/qjs_engine.rs |
RuntimeEngine — discover_skills(), start_skill(), call_tool(), list_skills() |
| Skill bootstrap | src/core/jsonrpc.rs |
bootstrap_skill_runtime() — reused to init QuickJS engine |
| Default state | src/core/jsonrpc.rs |
default_state() → AppState |
No logic duplication. The REPL is a thin input/output layer on top of the same internal APIs. Skill policy, parameter validation, secret redaction — all handled by the existing layers.
3.2 Workspace and Skills Registry Interaction
On startup the REPL:
- Resolves workspace from
OPENHUMAN_WORKSPACEenv or~/.openhuman(same as server). - Calls
bootstrap_skill_runtime()to initialize theRuntimeEngine, skills data dir, cron/ping schedulers — identical to server startup but without binding an HTTP port. - Skills source dir is resolved the same way (bundled
skills/skills/or workspace). - All
skill *commands go through the globalRuntimeEnginesingleton (set byset_global_engine()).
openhuman repl
├─ init logging (RUST_LOG or --verbose)
├─ bootstrap_skill_runtime() ← same as server
│ ├─ resolve workspace
│ ├─ create RuntimeEngine
│ ├─ set_global_engine()
│ └─ start cron + ping schedulers
├─ create Tokio runtime
├─ create rustyline Editor (history, completer)
└─ loop { readline → parse → invoke → print }
3.3 QuickJS Runtime Lifecycle
Skills run in QuickJS via QjsSkillInstance. The REPL shares the same lifecycle as the server:
skill start <id>→RuntimeEngine::start_skill()→ spawns QuickJS isolate + message loop.skill call <id> <tool> <args>→RuntimeEngine::call_tool()→ sendsSkillMessage::CallToolto the instance's mpsc channel → JS executes → returnsToolCallResult.skill stop <id>→ sendsSkillMessage::Stop→ QuickJS context dropped.- On REPL exit → all running instances are stopped (graceful shutdown).
No separate QuickJS management code is needed.
3.4 New Code Location
src/core/
repl.rs # REPL loop, line parsing, completion, formatting
cli.rs # Add "repl" match arm in run_from_cli_args()
mod.rs # Add `pub mod repl;`
Single new file (repl.rs, ~300-500 lines estimated). The cli.rs change is a one-line match arm.
3.5 New Dependency
# Cargo.toml
rustyline = { version = "15", features = ["with-file-history"] }
rustyline provides: line editing, history (persisted to ~/.openhuman/repl_history), tab completion, Ctrl-C/Ctrl-D handling, and cross-platform terminal support (including Windows).
4. Safety: Secrets, Tokens, and PII
Principles
-
No secrets in REPL output. The REPL displays results from
invoke_method()which already passes through the same code as the server. Existing RPC handlers are responsible for not returning raw secrets. -
Input redaction. The REPL must not log user-typed input at
infolevel or above if it may contain secrets (e.g.,--api_key,--plaintext). Debug logging of input is gated behind--verbose/RUST_LOG=debug. -
History file exclusions. Lines matching sensitive patterns are not written to the history file:
- Any line containing
api_key,token,secret,password,plaintext,mnemonic - Raw JSON with fields named
*key,*token,*secret
- Any line containing
-
Skill output.
call_tool()returnsToolCallResult { content, is_error }. The REPL printscontentas-is. Skill authors are responsible for not leaking credentials in tool output (same as in the desktop app). The REPL adds a one-line warning on firstskill call:note: skill tool output is printed verbatim; ensure skills do not emit secrets -
No JWT / session tokens. The REPL operates in-process with no auth layer. There are no session tokens to leak. Backend API calls (if any skill makes them) use credentials stored in the skills data dir, not typed interactively.
Implementation
fn should_skip_history(line: &str) -> bool {
let lower = line.to_lowercase();
const SENSITIVE: &[&str] = &[
"api_key", "token", "secret", "password",
"plaintext", "mnemonic", "private_key",
];
SENSITIVE.iter().any(|s| lower.contains(s))
}
5. Tab Completion
rustyline supports custom completers. The REPL provides context-aware completion:
| Position | Completes |
|---|---|
| First word | Namespace names, call, skill, schema, namespaces, env, help, exit |
| After namespace | Function names within that namespace |
After skill |
list, start, stop, status, tools, call |
After skill start/stop/status/tools |
Discovered skill IDs |
After skill call <id> |
Tool names from that skill's snapshot |
| After function name | --param_name flags from schema |
Completion data is derived from all_controller_schemas() and RuntimeEngine::list_skills() — no extra state.
6. Output Modes
| Mode | Default | Toggle | Behavior |
|---|---|---|---|
| Pretty | on | .json off |
Colored, indented JSON with field highlights |
| Raw JSON | off | .json on |
Machine-parseable serde_json::to_string_pretty |
| Timing | off | .time on |
Appends (Xms) after each result |
| Verbose | off | .verbose on |
Sets RUST_LOG=debug for the process |
In --batch / --eval mode, output defaults to raw JSON (machine-friendly).
7. Error Handling
openhuman> config update_model_settings
error: missing required param 'model_id'
hint: openhuman config update_model_settings --help
openhuman> skill start nonexistent
error: skill 'nonexistent' not found
hint: run `skill list` to see discovered skills
openhuman> skill call gmail bad_tool {}
error: tool 'bad_tool' not found in skill 'gmail'
hint: run `skill tools gmail` to see available tools
Errors print to stderr (red if tty), results to stdout. This makes --eval / --batch output clean for piping.
8. Follow-Up: Implementation Milestones
Phase 1 — MVP REPL (single PR)
openhuman replsubcommand with rustyline loop- Namespace/function dispatch via
invoke_method() call <method> [json]for raw RPChelp,schema,namespaces,env,exit.json,.verbose,.timetoggles- History file with sensitive-line exclusion
- Basic tab completion (namespaces + functions)
--evalsingle-command mode- Unit tests for line parsing and completion
Issue: to be created after design approval.
Phase 2 — Skills-Focused Commands
skill list/start/stop/status/tools/callshorthand commands- Tab completion for skill IDs and tool names
- Skill event streaming (print
skill-state-changedevents inline) skill setup <id>interactive setup flow- Skill output formatting (tool result → readable text)
Issue: to be created after Phase 1 merges.
Phase 3 — Script Mode & CI
--batchstdin mode (one command per line, raw JSON output)- Exit codes: 0 = all ok, 1 = any command failed
--evalsupports semicolon-separated commands- Example scripts in
docs/for common workflows - CI integration example (smoke test in GitHub Actions)
Issue: to be created after Phase 2.
Appendix: Alternatives Considered
A. REPL as a wrapper around HTTP (like curl)
Rejected. Adds network overhead, requires openhuman run to be running, and can't access the skill runtime's in-process state without the server. In-process invocation is simpler and faster.
B. Embed a full Lua/Python scripting layer
Rejected for v1. Over-engineered for the stated goals. The --eval / --batch modes give enough scriptability. Can revisit if demand appears.
C. TUI with panels (like lazygit)
Rejected for v1. Adds significant complexity (UI framework, layout, event handling). A line-based REPL covers the stated use cases. A TUI could be built on top later if warranted.
D. Use clap derive macros for REPL parsing
Rejected. clap is designed for one-shot CLI parsing, not interactive loops. The existing hand-rolled parser in cli.rs is a better fit; the REPL reuses parse_function_params() directly.