From c33ed0fbadbf52a1eee16f9ede2d0ce1797cc028 Mon Sep 17 00:00:00 2001 From: rasbt Date: Sun, 5 Apr 2026 12:03:43 -0500 Subject: [PATCH 1/2] Auto mode warning --- README.md | 2 +- mini_coding_agent.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 61ba020..78c1767 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Risky tools such as shell commands and file writes are gated by approval. - `--approval ask` prompts before risky actions (default and recommended) - `--approval auto` - allows risky actions automatically (convenient but riskier) + allows risky actions automatically, including arbitrary command execution and file writes by the model; use only with trusted prompts and trusted repositories - `--approval never` denies risky actions diff --git a/mini_coding_agent.py b/mini_coding_agent.py index f06d1f7..7e31c5d 100644 --- a/mini_coding_agent.py +++ b/mini_coding_agent.py @@ -956,7 +956,12 @@ def build_arg_parser(): parser.add_argument("--host", default="http://127.0.0.1:11434", help="Ollama server URL.") parser.add_argument("--ollama-timeout", type=int, default=300, help="Ollama request timeout in seconds.") parser.add_argument("--resume", default=None, help="Session id to resume or 'latest'.") - parser.add_argument("--approval", choices=("ask", "auto", "never"), default="ask", help="Approval policy for risky tools.") + parser.add_argument( + "--approval", + choices=("ask", "auto", "never"), + default="ask", + help="Approval policy for risky tools; auto grants the model arbitrary command execution and file writes.", + ) parser.add_argument("--max-steps", type=int, default=6, help="Maximum tool/model iterations per request.") parser.add_argument("--max-new-tokens", type=int, default=512, help="Maximum model output tokens per step.") parser.add_argument("--temperature", type=float, default=0.2, help="Sampling temperature sent to Ollama.") From 6a7061d89ebd4da01f011c8b3a5c67adcd329d00 Mon Sep 17 00:00:00 2001 From: rasbt Date: Sun, 5 Apr 2026 12:22:57 -0500 Subject: [PATCH 2/2] Add more warnings about auto mode --- .github/workflows/ci.yml | 60 +++++++++++++++++++++++++++++++++ mini_coding_agent.py | 15 +++++++-- tests/test_mini_coding_agent.py | 36 ++++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..df275bb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + name: ${{ matrix.os }} / Python ${{ matrix.python-version }} / ${{ matrix.installer }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.10" + installer: + - pip + - uv + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies with pip + if: matrix.installer == 'pip' + run: | + python -m pip install --upgrade pip + python -m pip install -e . pytest ruff + + - name: Install uv and sync environment + if: matrix.installer == 'uv' + run: | + python -m pip install --upgrade pip + python -m pip install uv + uv sync --group dev + + - name: Lint with pip + if: matrix.installer == 'pip' + run: python -m ruff check . + + - name: Run tests with pip + if: matrix.installer == 'pip' + run: python -m pytest -q + + - name: Lint with uv + if: matrix.installer == 'uv' + run: uv run python -m ruff check . + + - name: Run tests with uv + if: matrix.installer == 'uv' + run: uv run python -m pytest -q diff --git a/mini_coding_agent.py b/mini_coding_agent.py index 7e31c5d..236a3a4 100644 --- a/mini_coding_agent.py +++ b/mini_coding_agent.py @@ -1,6 +1,5 @@ import argparse import json -import os import re import shutil import subprocess @@ -734,11 +733,23 @@ def reset(self): self.session["memory"] = {"task": "", "files": [], "notes": []} self.session_store.save(self.session) + def path_is_within_root(self, resolved): + probe = resolved + while not probe.exists() and probe.parent != probe: + probe = probe.parent + for candidate in (probe, *probe.parents): + try: + if candidate.samefile(self.root): + return True + except OSError: + continue + return False + def path(self, raw_path): path = Path(raw_path) path = path if path.is_absolute() else self.root / path resolved = path.resolve() - if os.path.commonpath([str(self.root), str(resolved)]) != str(self.root): + if not self.path_is_within_root(resolved): raise ValueError(f"path escapes workspace: {raw_path}") return resolved diff --git a/tests/test_mini_coding_agent.py b/tests/test_mini_coding_agent.py index ee718b5..e5a4298 100644 --- a/tests/test_mini_coding_agent.py +++ b/tests/test_mini_coding_agent.py @@ -1,4 +1,5 @@ import json +import pytest from unittest.mock import patch from mini_coding_agent import ( @@ -188,6 +189,41 @@ def test_list_files_hides_internal_agent_state(tmp_path): assert "[F] hello.txt" in result +def test_path_rejects_parent_escape(tmp_path): + agent = build_agent(tmp_path, []) + + with pytest.raises(ValueError, match="path escapes workspace"): + agent.path("../outside.txt") + + +def test_path_rejects_symlink_escape(tmp_path): + agent = build_agent(tmp_path, []) + outside = tmp_path.parent / f"{tmp_path.name}-outside" + outside.mkdir() + link = tmp_path / "outside-link" + try: + link.symlink_to(outside, target_is_directory=True) + except (OSError, NotImplementedError): + pytest.skip("symlink creation is not available in this environment") + + with pytest.raises(ValueError, match="path escapes workspace"): + agent.path("outside-link/secret.txt") + + +def test_path_accepts_case_variant_on_case_insensitive_filesystems(tmp_path): + project_root = tmp_path / "Proj" + project_root.mkdir() + agent = build_agent(project_root, []) + variant = project_root.parent / project_root.name.lower() / "README.md" + + if not variant.exists(): + pytest.skip("case-sensitive filesystem") + + resolved = agent.path(str(variant)) + + assert resolved.samefile(project_root / "README.md") + + def test_repeated_identical_tool_call_is_rejected(tmp_path): agent = build_agent(tmp_path, []) agent.record({"role": "tool", "name": "list_files", "args": {}, "content": "(empty)", "created_at": "1"})