4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
17 KiB
17 KiB
Interactive shell package
These instructions apply to interactive_shell/ and all of its
subdirectories. The repo-root AGENTS.md still applies.
Purpose
interactive_shell/ owns the interactive OpenSRE terminal surface: the REPL
loop, slash-command surface, local alert ingestion, shell execution, and Rich /
prompt-toolkit UI. Reusable agent session state, prompt history, grounding, and
prompt construction live under core.agent.
Design for a terminal user who may be in the middle of an incident: behavior should be predictable, interruptible, explainable, and safe by default.
Package map and ownership
| Area | Owns | Keep out |
|---|---|---|
main.py |
process/bootstrap boundary for starting the REPL | per-turn dispatch/runtime logic |
controller.py |
top-level REPL wiring, alert listener lifecycle, prompt loop, background workers, and shutdown | feature-specific business logic or compatibility-only forwarding |
runtime/core/turn_accounting.py |
shell turn accounting (ShellTurnAccounting) for analytics, telemetry, recorder flush, turn persistence, and intent stamps |
turn-flow control (owned by core.agent_harness) or tool-calling turn execution |
command_registry/ |
slash-command definitions, argument validation, command dispatch | long-running implementation details better placed in services/runtime modules |
runtime/ |
background task workers, lifecycle/ReplState, runtime context assembly, semantic shell-turn execution, and core harness adapters |
prompt text, reusable session persistence, or compatibility shims |
tools/interactive_shell/shell/ |
shell command parsing, shell execution policy, subprocess execution, and the run_shell_command/run_cd/run_pwd runner (next to the shell_run tool in tools/interactive_shell/actions/shell.py) |
slash-command execution |
references/ |
CLI/docs/source/AGENTS reference loading and caching | generated model prose |
config/ |
interactive-shell config loading and tool catalog metadata | global app config unrelated to the REPL |
ui/ |
Rich/prompt-toolkit rendering, theme, menus, streaming output, and domain views such as incoming_alerts.py (receiver/queue/listener lifecycle lives in core.domain.alerts.inbox) |
business logic or network calls |
When a change crosses these boundaries, prefer extracting a small helper in the owning area rather than adding more logic to the caller.
Cross-cutting rules
- Treat every external input as untrusted: user prompt text, slash-command args, alert payloads, files read into prompts, history, subprocess output, model output, and integration metadata.
- Keep the interactive path responsive. Long-running work must be cancellable, timeout-bounded, moved off the input path, or surfaced with clear progress.
- Preserve import-time lightness. Do not start threads, call LLMs, read large files, or contact networks at module import time.
- Prefer explicit data models and typed helpers over loosely shaped dictionaries when data crosses submodule boundaries.
- Keep user-visible strings intentional. Slash-command names, flags, output labels, prompts, response bodies, and error wording are user-facing API.
- Avoid new module-level mutable globals. If global coordination is unavoidable, provide deterministic reset/cleanup hooks and test isolation.
- Do not keep compatibility-only forwarding modules after moving code. Migrate callers/tests to the canonical owner and remove the old import path in the same change.
Slash commands
- Add commands as
SlashCommandentries in the relevantcommand_registry/*module. Keep handlers small: parse args, call focused helpers, render result. - REPL + CLI parity (required): Every command in
SLASH_COMMANDSmust have a matching_MCP_BY_COMMANDentry incommand_registry/slash_catalog.py. That catalog feeds the LLM planner (slash_invoke), planner tool specs, and compact help text. Without it, CI fails (test_slash_catalog_covers_all_registered_commands).- New REPL-only slash command: add
SlashCommandin the owningcommand_registry/*module and_mcp(...)inslash_catalog.py(keep keys sorted alphabetically in_MCP_BY_COMMAND). - New CLI with REPL parity: add the Click command under
surfaces/cli/commands/, register aSlashCommandincommand_registry/cli_parity.py(subprocess toopensre …), and add_MCP_BY_COMMANDinslash_catalog.pywithllm_description,use_cases, andanti_examplesaligned to the command’susagetuple. - Verify before push:
uv run python -m pytest tests/interactive_shell/command_registry/test_slash_catalog.py -q
- New REPL-only slash command: add
- Use
validate_argsfor cheap pre-policy validation so bad arguments do not trigger confirmations or side effects. - Send command execution through the central dispatch and execution-policy
helpers. Do not bypass
execution_policy.pyfor new commands. - Alpha allow-all execution policy (current behavior): the REPL runs with
no command guardrails.
execution_policy.pyresolves every action toallowwith no confirmation prompt — all slash/opensrecommands, investigations, synthetic tests, code-agent launches, LLM runtime switches, and all shell commands run immediately, in any context (TTY or not, trust mode or not). There is no shell-command safety policy: the read-only/mutating/restricted classification and thedenyfloor were removed (shell_policy.pydeleted; parsing/policy/execution live undertools/shell/). Mutating commands (rm/mv/docker),restrictedcommands (sudo,systemctl,kill,dd, …), shell operators (| && ; > <), and command substitution all run; the!prefix is honored but optional. The only shell input still rejected is genuinely empty input (a bare!or whitespace). Do not re-add a shell allowlist or deny floor while in alpha — seedocs/interactive-shell-action-policy.md. The formerExecutionTierclassification was removed because it gated nothing under default-allow; if an opt-in stricter policy is reintroduced after alpha, gate it inexecution_policy.py(theaskverdict, confirmation UX, andtrust_modeare retained as the hook), not via a planner-stage denial. - Non-TTY behavior under default-allow: actions no longer fail closed on
non-interactive stdin (there is nothing to confirm). The fail-closed path only
applies if a verdict is explicitly
ask, which the default policy does not emit. - CPR / exclusive-stdin registration (required for table-outputting commands):
Under
patch_stdout(raw=True), the REPL runs dispatch concurrently with the nextprompt_async(). When a command emits Rich table output, prompt_toolkit redraws the prompt mid-flight, sending anESC[6nDSR query; the terminal's CPR response (ESC[<row>;<col>R) arrives as literal keystrokes in the incoming prompt buffer, causing garbage like^[[60;1Rto appear. Any command that callsprint_repl_table(directly or viarender_table/render_integrations_table/render_models_table/ etc.) must be added to_EXCLUSIVE_STDIN_MENU_COMMANDSinruntime/utils/input_policy.py. That makes the main loop callawait state.queue.join(), blocking the next prompt until dispatch completes and both drain cycles clean up stale CPR bytes before the nextprompt_async()starts.- How to check: after adding a command, run it in the REPL and type a few
characters in the next prompt. If no
^[[…Rgarbage appears, the registration is correct. - Agent-selected interactive commands:
_EXCLUSIVE_STDIN_MENU_COMMANDSonly reserves stdin for literal/slashcommand text that_literal_slash_command_textrecognizes. When free text like "remove github" is resolved by the action agent into an inline-picker command (/integrations remove,/integrations setup,/mcp connect,/mcp disconnect, or a bare/integrations//mcpmenu), the loop has not reserved stdin, sotools/interactive_shell/actions/slash.pymust NOT run the picker inline. It defers viasession.queue_auto_command(...), which re-submits the command as literal command text so the loop can reserve exclusive stdin before the agent path runs it. New raw-stdin picker/wizard commands the action agent can emit must be added to_INTERACTIVE_PICKER_MENUS/_INTERACTIVE_PICKER_SUBCOMMANDSintools/interactive_shell/actions/slash.py.
- How to check: after adding a command, run it in the REPL and type a few
characters in the next prompt. If no
Action Selection And Execution
- Hard boundary: do not add regex/keyword/fuzzy intent routing for natural
language, or any deterministic mapping from non-
/-prefixed prose to an action (e.g. "show integrations" ->/integrations). Engineers have been fired before for reintroducing intent heuristics that compete with the action agent.- Sanctioned exception: input the user types as a literal
/slashcommand is dispatched deterministically (a staticslash_invokecall incore/agent_harness/turns/action_driver.py), so slash commands keep working when the action-agent LLM is unavailable. This is an explicit-command bypass, not intent inference — it fires only when the message is a/command, and free-form text is still LLM-selected. Seedocs/interactive-shell-action-policy.md("Deterministic literal-/slashdispatch").
- Sanctioned exception: input the user types as a literal
- No planning-stage fail-closed safeguard (v0.1 decision). The second-phase
action agent never denies a turn. Because every terminal action is read-only,
an unmatched/ambiguous/chatty clause is not a safety risk — the agent executes
the clauses it can map and lets the rest fall through to the conversational
assistant. We removed the
denieddecision path, themark_unhandledplanner tool, theUNHANDLED:convention, and the "I couldn't safely decide actions" message because they caused frequent false denials (e.g. a conversational question that embedded a quoted, list-style directive) with no safety upside. Details and rationale live incore/agent_harness/AGENTS.md. If mutating actions are ever introduced, gate them with the execution-stage confirmation policy (tools/shared/execution_policy.py), not a planner-stage denial. - Keep deterministic command detection in
orchestration/for terminal UI policy only; use the action agent for slash/tool action selection. - Send uncertainty to a safe surface: help/chat or a clarification, not direct mutation or shell execution.
- LLM-generated text must never execute directly. Convert proposed actions into
explicit planned actions, show them to the user when appropriate, then execute
through
orchestration/and policy gates. - Keep action summaries human-readable and specific enough for confirmation UX and audit logs.
- When adding a new action type, test allowed, denied, and confirmation-required paths.
LLM prompts, grounding, and references
- Keep prompts bounded. Enforce size caps for docs, source chunks, histories, observations, alert text, and command output included in model context.
- Ground procedural/help answers in maintained references (
docs/, CLI help, AGENTS files, source snippets). If references do not support an answer, say so rather than inventing steps. - Do not include secrets in prompts. Redact or omit tokens, auth headers, env values, local credentials, and raw integration config.
- Keep prompt rules reusable in
core.agent_harness.promptsso chat/help/action surfaces use consistent terminology and formatting. - Reference caches should be deterministic, invalidatable when source files change, and cheap to rebuild in tests.
Terminal UI and rendering
- Escape user-controlled content before passing it to Rich markup
(
rich.markup.escape): alerts, command output, file paths, integration names, model/provider labels, errors, docs snippets, and model text that is not already intentionally rendered as Markdown. - Use semantic tokens from
ui/theme.py. Do not introduce raw hex colors, Rich named colors, or raw ANSI escapes outsideui/theme.pyunless a narrow prompt-toolkit compatibility path requires it. - Keep rendering helpers as pure as practical: accept data, return/render Rich objects, avoid reading config or mutating session state from UI modules.
- Any raw terminal-mode code must check TTY support and restore terminal state
in
finally. - Be careful mixing
prompt_toolkit.patch_stdout, Rich live rendering, and background output. Prefer append-only, paragraph-buffered, or throttled rendering paths that do not corrupt the editable prompt. - UI changes should handle narrow terminals, non-ASCII fallback where relevant, long text, empty states, and non-TTY automation.
Shell, subprocesses, and local system effects
- Shell execution changes belong under
shell/and must preserve parsing, quoting, timeout, redaction, and policy behavior. - Treat subprocess output as untrusted display text; escape it before Rich markup and cap what is retained or sent to prompts.
- Use explicit timeouts and clear cancellation behavior for subprocesses. Avoid waits that can hang the REPL indefinitely.
- Keep allow/deny decisions explainable. If a command is blocked, return a user-facing reason and a safe alternative when possible.
State, history, config, and background work
- Prefer explicit
Sessionfields for session state. Keep ownership clear: runtime owns lifecycle, history owns persistence, config owns shell-specific settings. - Background threads/tasks/listeners must have deterministic shutdown. Tests
should stop handles and workers in fixtures or
finallyblocks. - Protect shared queues and mutable session data with locks or single-owner discipline. Avoid check-then-act races around queues, cancellation flags, current tasks, and listener handles.
- History should avoid storing secrets or excessive payloads. Apply truncation and privacy policy consistently.
- Config loading should degrade gracefully with actionable errors; do not make the REPL unusable because an optional config or catalog source is missing.
External input and local listener safety
- Network-ish local surfaces such as
core.domain.alerts.inbox(started by the REPL entrypoint) must validate cheap request metadata before blocking reads or expensive parsing. - Never perform unbounded request-body reads. For alert POSTs specifically,
validate
Content-Lengthfirst, and only then read the bounded body:- non-numeric
Content-Lengthvalues makeint(...)raiseValueError; catch this and return400. - negative lengths must return
400;rfile.read(-1)reads until EOF rather than zero bytes, which can stall the single-threaded handler. - oversized positive lengths must return
413without attempting to read the advertised body.
- non-numeric
- Preserve clean unauthorized responses for real POST bodies by draining only a
bounded body before returning
401; this avoids close-with-unread-data resets on some platforms without allowing oversized pre-auth reads. - Keep request-size and malformed-header checks effective for both authenticated and unauthenticated callers.
- Keep non-loopback listener binding protected by a token. Use constant-time token comparison and never log bearer tokens, raw auth headers, or full alert payloads.
Testing expectations
- Put tests under
tests/interactive_shell/, mirroring the package area when useful (orchestration/,ui/, etc.). Never add tests under source packages. - For focused changes, run the closest tests, for example:
uv run python -m pytest tests/core/domain/alerts/test_inbox.pyuv run python -m pytest tests/interactive_shell/<area>/uv run python -m pytest tests/interactive_shell/
- Add regression tests for incident-prone edges: platform socket behavior, malformed input, non-TTY execution, cancellation, policy denial, prompt-size caps, Rich escaping, and background cleanup.
- Prefer deterministic tests over sleep-heavy tests. Use fake classifiers, fake sessions, fake consoles, monkeypatched subprocesses, and small fixtures.
- For UI work, test pure formatting/rendering helpers where possible and keep full REPL-loop tests minimal.
- For action-planning or execution-policy changes, test both safe fallback behavior and the intended positive path.
Change checklist
Before considering an interactive-shell change complete, check:
- Is the logic in the right submodule, with import-time side effects avoided?
- Is user-facing behavior preserved or intentionally documented?
- Are unsafe actions sent through execution policy with the correct tier?
- Are external inputs bounded, escaped, redacted, and timeout-protected?
- Do background resources shut down deterministically?
- Are focused tests added or updated under
tests/interactive_shell/? - If
SLASH_COMMANDSchanged, doesslash_catalog.pyinclude every command (REPL andcli_parity)? Runtest_slash_catalog.py.