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
276 lines
17 KiB
Markdown
276 lines
17 KiB
Markdown
# 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 command’s
|
||
`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`.
|