mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
* 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>
405 lines
15 KiB
Markdown
405 lines
15 KiB
Markdown
# 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.
|