Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

17 KiB
Raw Permalink Blame History

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 SlashCommand entries in the relevant command_registry/* module. Keep handlers small: parse args, call focused helpers, render result.
  • REPL + CLI parity (required): Every command in SLASH_COMMANDS must have a matching _MCP_BY_COMMAND entry in command_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 SlashCommand in the owning command_registry/* module and _mcp(...) in slash_catalog.py (keep keys sorted alphabetically in _MCP_BY_COMMAND).
    • New CLI with REPL parity: add the Click command under surfaces/cli/commands/, register a SlashCommand in command_registry/cli_parity.py (subprocess to opensre …), and add _MCP_BY_COMMAND in slash_catalog.py with llm_description, use_cases, and anti_examples aligned to the commands usage tuple.
    • Verify before push: uv run python -m pytest tests/interactive_shell/command_registry/test_slash_catalog.py -q
  • Use validate_args for 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.py for new commands.
  • Alpha allow-all execution policy (current behavior): the REPL runs with no command guardrails. execution_policy.py resolves every action to allow with no confirmation prompt — all slash/opensre commands, 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 the deny floor were removed (shell_policy.py deleted; parsing/policy/execution live under tools/shell/). Mutating commands (rm/mv/docker), restricted commands (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 — see docs/interactive-shell-action-policy.md. The former ExecutionTier classification was removed because it gated nothing under default-allow; if an opt-in stricter policy is reintroduced after alpha, gate it in execution_policy.py (the ask verdict, confirmation UX, and trust_mode are 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 next prompt_async(). When a command emits Rich table output, prompt_toolkit redraws the prompt mid-flight, sending an ESC[6n DSR query; the terminal's CPR response (ESC[<row>;<col>R) arrives as literal keystrokes in the incoming prompt buffer, causing garbage like ^[[60;1R to appear. Any command that calls print_repl_table (directly or via render_table / render_integrations_table / render_models_table / etc.) must be added to _EXCLUSIVE_STDIN_MENU_COMMANDS in runtime/utils/input_policy.py. That makes the main loop call await state.queue.join(), blocking the next prompt until dispatch completes and both drain cycles clean up stale CPR bytes before the next prompt_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 ^[[…R garbage appears, the registration is correct.
    • Agent-selected interactive commands: _EXCLUSIVE_STDIN_MENU_COMMANDS only reserves stdin for literal /slash command text that _literal_slash_command_text recognizes. 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 / /mcp menu), the loop has not reserved stdin, so tools/interactive_shell/actions/slash.py must NOT run the picker inline. It defers via session.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_SUBCOMMANDS in tools/interactive_shell/actions/slash.py.

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 /slash command is dispatched deterministically (a static slash_invoke call in core/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. See docs/interactive-shell-action-policy.md ("Deterministic literal-/slash dispatch").
  • 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 denied decision path, the mark_unhandled planner tool, the UNHANDLED: 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 in core/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.prompts so 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 outside ui/theme.py unless 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 Session fields 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 finally blocks.
  • 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-Length first, and only then read the bounded body:
    • non-numeric Content-Length values make int(...) raise ValueError; catch this and return 400.
    • 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 413 without attempting to read the advertised body.
  • 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.py
    • uv 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:

  1. Is the logic in the right submodule, with import-time side effects avoided?
  2. Is user-facing behavior preserved or intentionally documented?
  3. Are unsafe actions sent through execution policy with the correct tier?
  4. Are external inputs bounded, escaped, redacted, and timeout-protected?
  5. Do background resources shut down deterministically?
  6. Are focused tests added or updated under tests/interactive_shell/?
  7. If SLASH_COMMANDS changed, does slash_catalog.py include every command (REPL and cli_parity)? Run test_slash_catalog.py.