security: isolate release subprocess credentials (#333)
CI / lint (pull_request) Successful in 31s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-08 19:34:43 +00:00
1 parent 671c88d287
commit 7342b2e6de
4 files changed
+156 -17

No files matched your search

+74
View File
@@ -1,4 +1,5 @@
import json
import os
import sys
import threading
import time
@@ -44,6 +45,29 @@ def test_shell_runner_treats_metacharacters_as_literal_arguments(tmp_path):
assert not injected.exists()
def test_shell_runner_uses_only_the_explicit_environment(tmp_path, monkeypatch):
output = tmp_path / "environment.json"
monkeypatch.setenv("GITEA_TOKEN", "write-capable-secret")
monkeypatch.setenv("UNRELATED_SECRET", "also-secret")
result = ShellRunner().run(
[
sys.executable,
"-c",
"import json, os, sys; json.dump(dict(os.environ), open(sys.argv[1], 'w'))",
str(output),
],
tmp_path,
{"PATH": os.environ["PATH"], "EXPECTED_VALUE": "available"},
)
environment = json.loads(output.read_text())
assert result.returncode == 0
assert environment["EXPECTED_VALUE"] == "available"
assert "GITEA_TOKEN" not in environment
assert "UNRELATED_SECRET" not in environment
def test_shell_runner_times_out_and_terminates_spawned_children(tmp_path):
marker = tmp_path / "child-survived.txt"
child = (
@@ -215,6 +239,18 @@ def test_non_positive_command_deadlines_fail_before_claim(tmp_path, field, value
assert client.claimed == []
@pytest.mark.parametrize("name", ["GITEA_TOKEN", "RELEASE_REPO", "bad-name", "1START"])
def test_invalid_agent_environment_allowlist_fails_before_claim(tmp_path, name):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])
cfg = replace(config(tmp_path), agent_env_vars=(name,))
with pytest.raises(ValueError, match="invalid agent environment variable"):
ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=14)
assert client.claimed == []
def test_command_deadlines_are_configurable_from_cli(monkeypatch):
monkeypatch.setattr(sys, "argv", [
"release-engine",
@@ -222,6 +258,8 @@ def test_command_deadlines_are_configurable_from_cli(monkeypatch):
"--agent-timeout", "120",
"--test-timeout", "45.5",
"--termination-grace", "2",
"--agent-env", "CODEX_HOME",
"--agent-env", "HTTPS_PROXY",
])
args = _parse_args()
@@ -229,6 +267,7 @@ def test_command_deadlines_are_configurable_from_cli(monkeypatch):
assert args.agent_timeout == 120
assert args.test_timeout == 45.5
assert args.termination_grace == 2
assert args.agent_env == ["CODEX_HOME", "HTTPS_PROXY"]
def test_agent_failure_blocks_tests_and_pr(tmp_path):
@@ -306,6 +345,41 @@ def test_issue_content_reaches_agent_only_through_environment(tmp_path):
assert runner.environments[1]["RELEASE_ISSUE_BODY"] == body
def test_release_stages_receive_least_privilege_environments(tmp_path, monkeypatch):
monkeypatch.setenv("GITEA_TOKEN", "write-capable-secret")
monkeypatch.setenv("UNRELATED_SECRET", "also-secret")
monkeypatch.setenv("CODEX_HOME", "/approved/codex")
issue = Issue(14, "Engine MVP", "sensitive body", "open", [], None)
client = FakeClient([issue])
runner = FakeRunner([
CommandResult(0, "", ""),
CommandResult(0, "implemented", ""),
CommandResult(0, "abc123\n", ""),
CommandResult(0, "8 passed", ""),
CommandResult(0, "", ""),
])
cfg = replace(config(tmp_path), agent_env_vars=("CODEX_HOME",))
ReleaseEngine(cfg, client, runner).execute(issue_number=14)
for environment in runner.environments:
assert environment is not None
assert "PATH" in environment
assert "GITEA_TOKEN" not in environment
assert "UNRELATED_SECRET" not in environment
agent_environment = runner.environments[1]
assert agent_environment["CODEX_HOME"] == "/approved/codex"
assert agent_environment["RELEASE_ISSUE_NUMBER"] == "14"
assert agent_environment["RELEASE_ISSUE_TITLE"] == issue.title
assert agent_environment["RELEASE_ISSUE_BODY"] == issue.body
assert agent_environment["RELEASE_REPO"] == cfg.repo
assert agent_environment["RELEASE_BRANCH"] == "timmy/14-engine-mvp"
assert not any(
name.startswith("RELEASE_")
for name in runner.environments[3]
)
def test_failed_tests_block_push_and_pr(tmp_path):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])