# `--yolo` mode

Adds a single flag that disables the filesystem sandbox and the command whitelist, letting the agent read/write anywhere and run any command.

## Scope

Two things change when `--yolo` is active:

1. **File operations bypass sandboxing** — `read_file`, `write_file`, `edit_file`, `list_files`, and `grep` can target any path on disk, not just paths under `base_dir`.
2. **`run_command` accepts any command** — the tool is always available (no `--allowed-commands` required) and skips the resolved-commands whitelist lookup. The "commands inside workspace are dangerous" startup guard is also skipped.

Everything else stays the same: output caps, binary detection, timeout clamping, large-output spill-to-file, etc.

## Changes by file

### agent.py

- Add `--yolo` to `build_parser()` as a `store_true` flag. No mutual exclusion with `--allowed-commands` (if both are given, `--yolo` wins).
- In `main()`, when `args.yolo` is true:
  - Skip the `allowed_commands` resolution loop entirely (including the "commands inside workspace" startup guard — that check is meaningless when everything is unrestricted).
  - Set `resolved_commands = {}` (empty dict, not `None` — the `or {}` coercion in `handle_tool_call()` at line 221 would squash `None` to `{}` anyway, so use `{}` explicitly and rely on `yolo=True` to bypass the whitelist inside `_run_command()`).
  - Always include `RUN_COMMAND_TOOL` in the tools list, with its description set to `"Run any command and return its output."` (no "Allowed commands:" suffix).
- Pass `yolo=args.yolo` through to `handle_tool_call()`, which forwards it to `dispatch()` via kwargs.
- **System prompt text for commands** (lines 581–589): when `yolo` is true, append an unrestricted variant of the command-execution blurb instead of the "Allowed commands: ..." text. Something like:

  ```
  **Command execution tool:**
  - `run_command`: Run any command and return its output.
    Pass the command and arguments as a list (e.g. `["ls", "-la"]`).
    Optional `timeout` (1-120s, default 30).
  ```

  This replaces the current block that is gated on `if resolved_commands:`.

### tools.py

#### `safe_resolve()`

- Add an `unrestricted: bool = False` parameter.
- When `unrestricted` is true, resolve the path (still resolve symlinks for consistency) but skip the `is_relative_to` checks entirely. Return the resolved path unconditionally.

#### `_is_within_base()`

- Add an `unrestricted: bool = False` parameter.
- When `unrestricted` is true, return `True` unconditionally.
- This is needed because `_list_files` (line 424) and `_grep` (line 500) call `_is_within_base()` as a per-file containment check during directory walks, independent of `safe_resolve()`. Without this, files found via `os.walk` outside `base_dir` would be silently skipped even though the root directory passed `safe_resolve()`.

#### `_list_files`

- Thread `unrestricted` from `dispatch()` into both the `safe_resolve()` call (line 402) and the `_is_within_base()` call (line 424).
- **Path formatting** (line 442): currently formats output as `filepath.relative_to(base)`, which raises `ValueError` when `filepath` is outside `base_dir`. When `unrestricted` is true and the file is not relative to `base`, use the absolute path instead.

#### `_grep`

- Same treatment as `_list_files`:
  - Thread `unrestricted` into `safe_resolve()` (line 474) and `_is_within_base()` (line 500).
  - **Path formatting** (line 549): `filepath.relative_to(base)` → fall back to absolute path when outside base.

#### `_read_file`, `_write_file`, `_edit_file`

- Thread `unrestricted` into their `safe_resolve()` calls. No other changes needed — these don't have secondary containment checks or `relative_to` formatting.

#### `_run_command()`

- Add an `unrestricted: bool = False` parameter.
- When `unrestricted` is true:
  - Skip the "reject paths in command[0]" check.
  - Skip the `resolved_commands` lookup.
  - Resolve `command[0]` via `shutil.which()` at call time (so bare names still work), or use it as-is if it contains a `/`. If `shutil.which()` returns `None` for a bare name, return `"error: command not found on PATH: {cmd_name!r}"` immediately — don't let it fall through to `Popen` where it would produce a less clear `FileNotFoundError`.
  - Everything else (timeout clamping, output capture, large-output spill) stays.

#### `dispatch()`

- Read `yolo = kwargs.get("yolo", False)` once at the top.
- Pass `unrestricted=yolo` to `safe_resolve()` (via the wrapper functions), `_is_within_base()`, and `_run_command()`.
- When `yolo` is true and `name == "run_command"`, don't require `resolved_commands` to be present (it will be `{}`, and `_run_command` will skip the lookup).

### system_prompt.txt

No changes needed. The system prompt doesn't mention sandboxing or command restrictions. The command blurb is dynamically generated in `agent.py` (see the agent.py section above for the yolo-mode variant).

### Tests

Add `tests/test_yolo.py`:

**safe_resolve / _is_within_base:**
- `test_safe_resolve_unrestricted` — `safe_resolve()` with `unrestricted=True` returns a path outside `base_dir` without raising.
- `test_is_within_base_unrestricted` — `_is_within_base()` with `unrestricted=True` returns `True` for any path.

**File operations outside base_dir:**
- `test_read_outside_base_dir` — `_read_file` with `unrestricted=True`, reading a file outside `base_dir` succeeds.
- `test_write_outside_base_dir` — `_write_file` with `unrestricted=True`, writing to a path outside `base_dir` succeeds (use a second tmpdir).
- `test_edit_outside_base_dir` — `_edit_file` with `unrestricted=True`, editing a file outside `base_dir` succeeds.
- `test_list_files_outside_base_dir` — `_list_files` with `unrestricted=True`, listing a directory outside `base_dir` returns results (not empty). Verify output uses absolute paths.
- `test_grep_outside_base_dir` — `_grep` with `unrestricted=True`, grepping in a directory outside `base_dir` returns matches. Verify output uses absolute paths.

**run_command:**
- `test_run_any_command` — `_run_command()` with `unrestricted=True` runs a command not in `resolved_commands`.
- `test_run_command_with_path` — `_run_command()` with `unrestricted=True` accepts `/bin/echo` as `command[0]`.
- `test_run_command_not_found_unrestricted` — `_run_command()` with `unrestricted=True` and a nonexistent bare name (e.g. `["no_such_cmd_xyz"]`) returns `"error: command not found on PATH: 'no_such_cmd_xyz'"` instead of a `FileNotFoundError`.
- `test_yolo_overrides_allowed_commands` — when both `--yolo` and `--allowed-commands` are given, any command runs (not just the listed ones).

**Agent-level integration:**
- `test_yolo_tool_list_includes_run_command` — with `yolo=True` and no `--allowed-commands`, the tools list includes `run_command`.
- `test_yolo_system_prompt_text` — the system prompt contains the unrestricted command blurb, not the "Allowed commands:" text.

## What stays the same

- Output caps (50 KB read, 2000 char/line, 1 MB command output) — these protect context, not security.
- Binary file detection.
- Timeout clamping (1–120 s).
- Large output spill-to-file in `.swival/`.
- All other CLI flags.
- The `--no-instructions` flag (orthogonal concern).

## Design decisions

- **`resolved_commands = {}`, not `None`**: avoids the `or {}` coercion in `handle_tool_call()` silently eating the sentinel. The `unrestricted` flag on `_run_command()` is the real bypass mechanism.
- **`_is_within_base()` gets the `unrestricted` flag**: `list_files` and `grep` do per-file containment checks via `_is_within_base()` during `os.walk`, separate from the initial `safe_resolve()` on the root path. Both must be bypassed.
- **Absolute paths for outside-base output**: when `list_files`/`grep` encounter files outside `base_dir`, `relative_to(base)` would raise. Fall back to absolute paths in unrestricted mode.
- **Startup workspace guard skipped**: the check that rejects `--allowed-commands` entries resolving inside `base_dir` is irrelevant in yolo mode — the whole point is no restrictions.

## Migration / compatibility

None. New opt-in flag, no existing behavior changes.
