docs: REPL / interactive shell design (#92) (#96)

* 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>
This commit is contained in:
Steven Enamakel
2026-03-30 14:57:06 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 14dc860a21
commit fe277e1ef2
11 changed files with 1552 additions and 12 deletions
+404
View File
@@ -0,0 +1,404 @@
# Design: `openhuman repl` — Interactive Shell for Core Flows & Skills
**Issue**: [tinyhumansai/openhuman#92](https://github.com/tinyhumansai/openhuman/issues/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
```bash
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
```bash
# 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:
1. Resolves workspace from `OPENHUMAN_WORKSPACE` env or `~/.openhuman` (same as server).
2. Calls `bootstrap_skill_runtime()` to initialize the `RuntimeEngine`, skills data dir, cron/ping schedulers — identical to server startup but **without** binding an HTTP port.
3. Skills source dir is resolved the same way (bundled `skills/skills/` or workspace).
4. All `skill *` commands go through the global `RuntimeEngine` singleton (set by `set_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()` → sends `SkillMessage::CallTool` to the instance's mpsc channel → JS executes → returns `ToolCallResult`.
- `skill stop <id>` → sends `SkillMessage::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
```toml
# 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
1. **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.
2. **Input redaction.** The REPL must **not** log user-typed input at `info` level or above if it may contain secrets (e.g., `--api_key`, `--plaintext`). Debug logging of input is gated behind `--verbose` / `RUST_LOG=debug`.
3. **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`
4. **Skill output.** `call_tool()` returns `ToolCallResult { content, is_error }`. The REPL prints `content` as-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 first `skill call`:
```
note: skill tool output is printed verbatim; ensure skills do not emit secrets
```
5. **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
```rust
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 repl` subcommand with rustyline loop
- [ ] Namespace/function dispatch via `invoke_method()`
- [ ] `call <method> [json]` for raw RPC
- [ ] `help`, `schema`, `namespaces`, `env`, `exit`
- [ ] `.json`, `.verbose`, `.time` toggles
- [ ] History file with sensitive-line exclusion
- [ ] Basic tab completion (namespaces + functions)
- [ ] `--eval` single-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/call` shorthand commands
- [ ] Tab completion for skill IDs and tool names
- [ ] Skill event streaming (print `skill-state-changed` events 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
- [ ] `--batch` stdin mode (one command per line, raw JSON output)
- [ ] Exit codes: 0 = all ok, 1 = any command failed
- [ ] `--eval` supports 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.