Files
tracer-cloud--opensre/docs/interactive-shell-action-policy.md
T
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

259 lines
14 KiB
Markdown

# Interactive Shell Action Policy (ADR)
## Status
Superseded — Jun 18, 2026. The declarative-rule-pack deterministic mapper and
the regex-based planner postprocessing overrides described in the original
decision have been removed. See "Decision (current): LLM is the sole tool
selector" below. The original decision is retained for historical context.
## Context
The interactive-shell action policy had grown through layered heuristics in
single modules: a regex/keyword deterministic mapper inferred tools from
free-form text, and planner postprocessing rewrote the model's chosen actions
with more regex. These heuristics competed with the LLM and caused
misclassifications (e.g. "investigate a sample test alert?" being treated as an
informational question instead of running the sample alert), and they were a
recurring source of precedence drift.
## Decision (current): The shell action agent is the sole tool selector
1. There is no regex/keyword intent inference. Non-command turns are
selected entirely by the shell action agent via native tool-calling.
2. Tool selection is driven by the action-agent system prompt
(`core/agent_harness/prompts/action_agent_prompt.py`) and the per-tool descriptions
in the tool catalog (`tools/interactive_shell/*`). Keep both precise — they
are the only selection signal.
3. The action path does not post-hoc rewrite the model's tool calls. Tool calls
execute as first-class `AgentTool`s through the shared `core`
tool-calling loop; argument shape and availability are enforced by the
AgentTool runtime contract and per-tool gates.
4. When the action-agent prompt overflows the context window, the turn falls
through to a conversational reply rather than guessing an action. When the
action-agent LLM itself is unavailable, the REPL renders and persists a
failed assistant turn so `/resume` can show the outage.
5. Literal `/slash` command text the user types verbatim is dispatched
deterministically, without the action-agent LLM (see the "Deterministic
literal-`/slash` dispatch" addendum below). This is an explicit-command
bypass, not natural-language intent inference: free-form text is still
selected entirely by the action agent. The runtime's literal-`/slash`
detection in `runtime/utils/input_policy._literal_slash_command_text` remains
terminal-UI policy (spinner suppression and exclusive-stdin gating); the
execution-side deterministic dispatch lives in
`core/agent_harness/turns/action_driver.py`.
## What this means for changes
- To change how a phrasing maps to a tool, edit the action-agent system prompt and/or
the relevant tool description — never add a regex.
- To add a new tool, add it to the tool catalog with a clear, self-describing
`description` and `input_schema`; the action agent selects it from that text
and receives it as an AgentTool.
- Live turn scenarios under
`tests/core/agent/scenarios/` are the regression
surface for action-agent behavior. Deterministic scenarios (`intent_class:
deterministic`) assert literal command dispatch only.
## Original decision (historical, superseded)
1. Deterministic mapping was split into declarative rule packs with one explicit precedence table.
2. Rule matching windows were named typed strategies instead of inline numeric slices.
3. Planner postprocessing ran as pure transforms over a typed `PlannerState`.
4. Fail-closed policy transforms and normalization transforms were registered separately and executed in one ordered list.
5. Legacy planner-result tuple compatibility was collapsed behind a single adapter.
6. Planner contracts included policy-trace artifacts to detect silent precedence drift.
## Integration awareness and LLM-driven read-only discovery
Addendum — Jun 18, 2026.
Factual questions about live state (for example "is sentry installed?") are
answered without adding keyword/regex rules. Two complementary mechanisms:
1. Context grounding (not action planning). At REPL boot, `run_repl_async`
(`surfaces/interactive_shell/main.py`) hydrates
`session.configured_integrations` from the shared
`configured_integration_services()` helper in `integrations/catalog.py`
(the same source the welcome banner uses, so they never diverge). The chat
assistant prompt (`build_environment_block` in
`core/agent_harness/prompts/assistant.py`) lists the configured set as
facts, letting the model answer directly when state is already known.
2. LLM-driven discovery. The action-agent system prompt
(`core/agent_harness/prompts/action_agent_prompt.py`) lets the model, at its own
discretion, emit a read-only discovery action (for example
`slash_invoke("/integrations", ["list"])` or `["verify"]`) to discover the
answer instead of deflecting. There is no keyword mapping for this — the LLM
decides. Under the alpha allow-all policy every discovery action runs without
confirmation (`execution_policy.allow_tool("slash")` returns `allow`); the
former `ExecutionTier`/`resolve_slash_execution_tier` classification was
removed because it gated nothing. No fail-closed regex rule is involved; the
action agent decides whether to emit a discovery action.
### Observe→answer summary loop
Addendum — Jun 18, 2026.
When the action agent runs a read-only discovery command to answer a question (e.g.
the user asks "is sentry installed?" and the model runs `/integrations`), the
raw command output (a verification table) is not a direct answer on its own.
The pipeline now follows up with a short assistant pass that summarizes that
output:
1. Read-only discovery slash commands stash a compact text view of what they
found on `session.agent.last_observation`
(`_record_integrations_observation` in
`surfaces/interactive_shell/command_registry/integrations.py`).
2. `run_agent_prompt` resets that field at the start of every action-agent
turn and, when a discovery command produced an observation and succeeded,
calls the conversational assistant with `tool_observation=...`
(inside the handled-turn observation branch in `pipeline.py`). The assistant
summarizes the output into a direct answer and is instructed not to emit
further actions.
This only fires when the action-agent tool path executes a read-only discovery command
and records an observation. The pipeline no longer has a pre-agent deterministic
dispatch branch.
Discovery commands also no longer dump validator stack traces into the REPL: a
vendor/config failure during verification (for example a GitHub MCP `401`) is
logged as a one-line warning instead of a full traceback, because
`report_validation_failure` now defaults to `include_traceback=False` while still
capturing the exception to Sentry.
### Auto-launching interactive setup ("can you configure X?")
Addendum — Jun 18, 2026.
When the user asks to configure, connect, set up, or add an integration
("can you configure sentry?", "connect datadog"), the action agent does not just
hand off to the conversational assistant — it launches the setup wizard for
them. The action agent emits a `slash_invoke` tool call for
`/integrations setup <service>` or `/mcp connect <server>`. The model chooses
the service; there is no per-vendor hardcoding.
The setup wizard is a child process that needs exclusive stdin, so it cannot run
inline mid-turn (the live prompt is competing for stdin). Instead
`tools/interactive_shell/actions/slash.py` queues the command via
`session.queue_auto_command(...)`, which prefills the next prompt and marks it
for auto-submit. The prompt refresh hook
(`wire_prompt_refresh` in `surfaces/interactive_shell/ui/input_prompt/refresh.py`) then submits it, so the
command flows through the normal exclusive-stdin turn path of the REPL
(`turn_needs_exclusive_stdin` recognizes `/integrations setup`) — the only
place an interactive child process gets clean stdin. In a non-TTY/scripted
context (no prompt to submit into), the slash command path degrades to normal
non-interactive slash behavior.
### Removal of the planning-stage fail-closed safeguard (v0.1)
Addendum — Jun 18, 2026.
The action agent does not deny a turn. Previously, any clause
the old planner could not map to an executable tool — flagged via the `mark_unhandled`
tool, an `UNHANDLED:` text marker, or an unavailable tool call — collapsed the
whole turn into a hard denial that printed *"I couldn't safely decide actions for
that request."* In practice this fired on legitimate input (most often a
conversational question that embedded a quoted, list-style directive such as
*figure out why X is crashing by querying (a) sentry, (b) github, (c) posthog*),
producing a dead end with no safety benefit.
Every terminal action in v0.1 is **read-only**, so an unmatched, ambiguous, or
chatty clause is not a safety risk. The action agent now:
- runs every clause it *can* map to an executable action, and
- lets everything else fall through to the conversational assistant (or simply
drops a chatty clause in a compound request).
Removed as part of this change: the `denied` field on `ActionPlanningDecision`,
`enforce_plan_fail_closed_policy`, `normalize_terminal_plan`, `render_plan_denied`,
the `mark_unhandled` planner tool, and the `UNHANDLED:` convention. The
`fail_closed`, `has_unhandled_clause`, and `turn.expected_signals` fields were
also removed from turn scenario fixtures, since the oracle never asserted on
them; the fixture `policy` block now carries a single `executes_terminal_action`
`boolean` (true only when a shell action AgentTool is expected to run).
If write/mutating actions are introduced later, gate them with the
execution-stage confirmation policy (`tools/interactive_shell/shared/execution_policy.py`), **not**
an action-selection denial.
### Removal of the shell-command safety policy (alpha)
Addendum — Jun 27, 2026.
**Decision:** while OpenSRE is in **alpha**, the interactive REPL runs **every**
shell command with **no guardrails**. The shell-command safety policy — the
read-only / mutating / restricted classification, the command allowlist, and the
hard `deny` floor — has been removed. This is a deliberate trade-off: alpha
prioritizes developer velocity over command sandboxing, and the REPL already
runs on the developer's own machine with their own privileges.
What changed:
- `shell_policy.py` (classification, allowlists, `classify_command`,
`evaluate_policy`, `PolicyDecision`) was deleted. The pure parsing helpers it
also contained moved to `tools/shell/parsing.py` (`parse_shell_command`,
`argv_for_repl_builtin_detection`, `ParsedShellCommand`), alongside the shell
execution policy in `tools/shell/policy.py`.
- `tools.shell.policy.evaluate_shell_from_parsed` now returns `allow` for every
command — read-only, mutating, `restricted` (`sudo`, `systemctl`, `kill`,
`dd`, …), shell operators (`| && ; > <`), and command substitution
(`` ` ``/`$(...)`). Commands that need a shell run through one automatically;
the `!` prefix is still honored but no longer required to escape the old
operator block.
- The **only** remaining non-execution outcome is genuinely empty input (a bare
`!` or whitespace), which is rejected as input validation, not as a guardrail.
The `ask`/confirmation machinery (`trust_mode` plus the confirmation UX) is
retained as an unused hook, split across two layers: the pure decision lives in
`tools/interactive_shell/shared/execution_policy.py` (`resolve_confirmation`), and the terminal
interaction (`execution_allowed` — console output, the `Proceed? [Y/n]` prompt,
analytics) lives in `surfaces/interactive_shell/ui/execution_confirm.py`. If command
guardrails are reintroduced after alpha, gate them here at the execution stage —
never with an action-selection denial in the planner.
### Deterministic literal-`/slash` dispatch (no LLM)
Addendum — Jun 28, 2026.
**Decision:** input the user types as a literal `/slash` command is dispatched
deterministically, **without consulting the action-agent LLM**. This supersedes
the earlier "the literal-`/slash` detection must never become an action
execution shortcut" wording.
**Why:** all REPL turns previously routed through the action-agent LLM, so when
that LLM was unavailable (a provider with no credit, a failed auth, an outage)
every slash command failed — including the exact commands needed to recover
(`/login`, `/auth`, `/onboard`, `/model`). That is a deadlock: you could not log
in because logging in required the LLM you were trying to fix. Typed commands
should not depend on a funded LLM.
**Scope — the line that keeps the original concern intact.** The original ADR
removed regex/keyword heuristics because they *inferred intent from natural
language* and competed with the LLM. This bypass does the opposite: it fires
**only** when the message text itself is a literal `/command` the user typed
verbatim. There is no inference. Free-form natural language ("log me in",
"show my integrations") is still selected entirely by the action agent. The line
is "explicit typed command" vs "natural language", identical to the line the
terminal-UI policy (`_literal_slash_command_text`) already draws.
**How it works.** `core/agent_harness/turns/action_driver.run_action_agent_turn` recognizes
literal `/slash` input and emits a deterministic `slash_invoke` tool call through
the same static-LLM path as the explicit `!cmd` shell escape
(`_StaticToolCallLLM`). Execution then flows through the normal `slash_invoke`
AgentTool → `dispatch_slash`, so recording, execution policy, exclusive-stdin
gating, pickers, and exit behavior are identical to an LLM-selected slash call —
the only difference is the tool *selection* is deterministic instead of
LLM-driven. The bypass no-ops (falls back to the LLM path) when `slash_invoke` is
not an available tool that turn.
**Consequences.**
- Literal slash commands are faster, free, and reliable even with no LLM credit.
- Compound requests that *start with* a literal slash are dispatched as that one
command (a single `slash_invoke`); compound phrasing that does not start with a
slash (e.g. `run /health and then investigate …`) is unaffected and still
LLM-routed. No turn scenario uses a prompt beginning with a literal `/`.
- A literal discovery command (e.g. `/integrations list`) shows its raw output
directly; the optional LLM summary pass only runs if the action-agent LLM is
available, and degrades cleanly (no summary) when it is not.
**Still forbidden:** regex/keyword/fuzzy intent routing for natural language,
post-hoc rewriting of LLM-selected tool calls, and any deterministic mapping from
non-`/`-prefixed text to an action. Those compete with the LLM and were removed
for good reason.