test_env_isolation.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. #!/usr/bin/env python3
  2. """Guard the clean-room env contract in run_evals.py and run_triggers.py.
  3. The eval result is only honest if nothing from the host shell leaks into the
  4. subprocess. Both scripts carry their own build_case_env (they are
  5. deliberately self-contained); this test pins the contract on both copies:
  6. exactly PATH + fresh HOME + CLAUDE_CONFIG_DIR + auth-var-only-when-set +
  7. declared passthrough keys, nothing else.
  8. Run with: python3 -m pytest test_env_isolation.py
  9. (or plain `python3 test_env_isolation.py` for a lightweight self-check).
  10. """
  11. import sys
  12. from pathlib import Path
  13. SCRIPTS_DIR = Path(__file__).resolve().parents[1]
  14. sys.path.insert(0, str(SCRIPTS_DIR))
  15. import run_evals # noqa: E402
  16. import run_triggers # noqa: E402
  17. BUILDERS = [run_evals.build_case_env, run_triggers.build_case_env]
  18. HOST_ENV = {
  19. "PATH": "/usr/bin:/bin",
  20. "HOME": "/Users/host",
  21. "ANTHROPIC_API_KEY": "sk-test-123",
  22. "AWS_SECRET_ACCESS_KEY": "host-secret-must-not-leak",
  23. "CLAUDE_CONFIG_DIR": "/Users/host/.claude",
  24. "EXTRA_VAR": "extra",
  25. }
  26. HOME = Path("/tmp/eval-case/.home")
  27. def test_minimal_env_keys():
  28. adapter = {"auth_env": "ANTHROPIC_API_KEY"}
  29. for build in BUILDERS:
  30. env = build(adapter, HOME, HOST_ENV)
  31. assert set(env) == {"PATH", "HOME", "CLAUDE_CONFIG_DIR",
  32. "ANTHROPIC_API_KEY"}, (build.__module__, env)
  33. assert env["PATH"] == HOST_ENV["PATH"]
  34. assert env["HOME"] == str(HOME), "HOME must be the fresh case home"
  35. assert env["CLAUDE_CONFIG_DIR"] == str(HOME / ".claude")
  36. assert env["ANTHROPIC_API_KEY"] == "sk-test-123"
  37. assert "AWS_SECRET_ACCESS_KEY" not in env, "host secrets leaked"
  38. def test_auth_var_absent_when_unset():
  39. # Setting auth to "" breaks the runtime's OAuth fallback — the key must
  40. # be absent, never empty.
  41. adapter = {"auth_env": "ANTHROPIC_API_KEY"}
  42. for host in ({}, {"ANTHROPIC_API_KEY": ""}):
  43. for build in BUILDERS:
  44. env = build(adapter, HOME, {"PATH": "/bin", **host})
  45. assert "ANTHROPIC_API_KEY" not in env, (build.__module__, env)
  46. def test_no_adapter_still_minimal():
  47. for build in BUILDERS:
  48. env = build(None, HOME, HOST_ENV)
  49. assert set(env) == {"PATH", "HOME", "CLAUDE_CONFIG_DIR"}, env
  50. def test_env_passthrough_only_declared_and_present():
  51. adapter = {"auth_env": "ANTHROPIC_API_KEY",
  52. "env_passthrough": ["EXTRA_VAR", "NOT_SET_ON_HOST"]}
  53. for build in BUILDERS:
  54. env = build(adapter, HOME, HOST_ENV)
  55. assert env.get("EXTRA_VAR") == "extra"
  56. assert "NOT_SET_ON_HOST" not in env
  57. assert "AWS_SECRET_ACCESS_KEY" not in env
  58. if __name__ == "__main__":
  59. test_minimal_env_keys()
  60. test_auth_var_absent_when_unset()
  61. test_no_adapter_still_minimal()
  62. test_env_passthrough_only_declared_and_present()
  63. print("ok: build_case_env contract holds in run_evals and run_triggers")