Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions coding-agent/src/coding_agent/tools/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
from agentkit.tools import tool

_DISALLOWED_TOKENS = {"&&", "||", "|", ";", ">", ">>", "<", "2>", "&"}
_REMOTE_URL_SCHEMES = (
"https://",
"http://",
"git+https://",
"git+http://",
"git+ssh://",
"ssh://",
"git://",
)

_STRUCTURED_RESULTS: ContextVar[bool] = ContextVar(
"coding_agent_shell_structured_results", default=False
)
Expand Down Expand Up @@ -295,12 +305,22 @@ def _validated_execution_cwd(
return str(resolved_cwd)


def _looks_like_remote_url(arg: str) -> bool:
stripped = arg.strip().strip("'\"")
lowered = stripped.lower()
if lowered.startswith(_REMOTE_URL_SCHEMES):
return True
return lowered.startswith("git@") and ":" in stripped[4:]


def _validate_no_path_escape(
args: list[str],
workspace_root: Path,
additional_roots: tuple[Path, ...] = (),
) -> None:
for arg in args[1:]:
if _looks_like_remote_url(arg):
continue
for match in re.findall(r"(?<![A-Za-z0-9_.-])/[A-Za-z0-9_./-]+", arg):
candidate = Path(match).expanduser().resolve()
if not _path_under_any_root(candidate, workspace_root, additional_roots):
Expand Down
85 changes: 85 additions & 0 deletions coding-agent/tests/tools/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,91 @@ def test_none_sandbox_blocks_absolute_path_escape(self, tmp_path: Path):
assert str(workspace).lower() in blocked_text
assert "secret" not in blocked_text


def test_none_sandbox_allows_https_git_url_argument(self, tmp_path: Path):
workspace = tmp_path / "workspace"
workspace.mkdir()
pipeline_ctx = SimpleNamespace(
config={"workspace_root": str(workspace), "shell": {"sandbox_mode": "none"}}
)
url = "https://github.com/delete-cloud/Coding-Agent-Loop.git"
result = bash_run(
command=f"echo {url}",
cwd=str(workspace),
__pipeline_ctx__=pipeline_ctx,
)
result_text = _as_text(result)
assert "outside sandbox workspace" not in result_text.lower()
assert url in result_text

def test_none_sandbox_allows_ssh_git_url_argument(self, tmp_path: Path):
workspace = tmp_path / "workspace"
workspace.mkdir()
pipeline_ctx = SimpleNamespace(
config={"workspace_root": str(workspace), "shell": {"sandbox_mode": "none"}}
)
url = "git@github.com:delete-cloud/Coding-Agent-Loop.git"
result = bash_run(
command=f"echo {url}",
cwd=str(workspace),
__pipeline_ctx__=pipeline_ctx,
)
result_text = _as_text(result)
assert "outside sandbox workspace" not in result_text.lower()
assert "github.com:delete-cloud/Coding-Agent-Loop.git" in result_text

def test_none_sandbox_allows_ssh_scheme_git_url_argument(self, tmp_path: Path):
workspace = tmp_path / "workspace"
workspace.mkdir()
pipeline_ctx = SimpleNamespace(
config={"workspace_root": str(workspace), "shell": {"sandbox_mode": "none"}}
)
url = "ssh://git@github.com/delete-cloud/Coding-Agent-Loop.git"
result = bash_run(
command=f"echo {url}",
cwd=str(workspace),
__pipeline_ctx__=pipeline_ctx,
)
result_text = _as_text(result)
assert "outside sandbox workspace" not in result_text.lower()
assert url in result_text

def test_none_sandbox_allows_git_plus_ssh_url_argument(self, tmp_path: Path):
workspace = tmp_path / "workspace"
workspace.mkdir()
pipeline_ctx = SimpleNamespace(
config={"workspace_root": str(workspace), "shell": {"sandbox_mode": "none"}}
)
url = "git+ssh://git@github.com/delete-cloud/Coding-Agent-Loop.git"
result = bash_run(
command=f"echo {url}",
cwd=str(workspace),
__pipeline_ctx__=pipeline_ctx,
)
result_text = _as_text(result)
assert "outside sandbox workspace" not in result_text.lower()
assert url in result_text

def test_none_sandbox_blocks_escape_path_next_to_git_url(self, tmp_path: Path):
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = tmp_path / "outside"
outside.mkdir()
pipeline_ctx = SimpleNamespace(
config={"workspace_root": str(workspace), "shell": {"sandbox_mode": "none"}}
)
result = bash_run(
command=(
"git clone https://github.com/delete-cloud/Coding-Agent-Loop.git "
f"{outside}"
),
cwd=str(workspace),
__pipeline_ctx__=pipeline_ctx,
)
result_text = _as_text(result).lower()
assert "outside sandbox workspace" in result_text
assert str(workspace).lower() in result_text

def test_none_sandbox_allows_absolute_path_under_additional_root(
self, tmp_path: Path
):
Expand Down
Loading