30b4f12288
Split pipelock config building from YAML rendering: pipelock_build_config returns a dict, pipelock_render_yaml serializes it, and _build_pipelock_yaml chains the two onto disk. Unchanged behavior — pipelock loads the same YAML. The yaml test now asserts on the structured config dict, which is robust to cosmetic YAML changes (key order, quoting). The two checks that only make sense on the rendered output — file mode 0600 and no-secret-leakage — stay against the on-disk content. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
98 lines
3.7 KiB
Python
98 lines
3.7 KiB
Python
"""Unit: pipelock config building and YAML rendering.
|
|
|
|
`pipelock_build_config` produces the structured config dict pipelock
|
|
will load; tests assert on that dict so they don't break on cosmetic
|
|
YAML changes. A small set of tests still hit the rendered output for
|
|
properties that only make sense on disk (file mode, no-secret-leakage).
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
|
|
from claude_bottle.manifest import Manifest
|
|
from claude_bottle.pipelock import pipelock_build_config, pipelock_render_yaml
|
|
from tests.fixtures import fixture_minimal, fixture_with_ssh
|
|
|
|
|
|
class TestBuildConfig(unittest.TestCase):
|
|
def test_minimal_shape(self):
|
|
cfg = pipelock_build_config(fixture_minimal().bottles["dev"])
|
|
self.assertEqual("strict", cfg["mode"])
|
|
self.assertEqual(True, cfg["enforce"])
|
|
self.assertEqual({"enabled": True}, cfg["forward_proxy"])
|
|
self.assertEqual(
|
|
{"include_defaults": True, "scan_env": True}, cfg["dlp"]
|
|
)
|
|
# Baked defaults always present.
|
|
self.assertIn("api.anthropic.com", cfg["api_allowlist"])
|
|
self.assertIn("raw.githubusercontent.com", cfg["api_allowlist"])
|
|
# No SSH entries → no trusted_domains, no ssrf.
|
|
self.assertNotIn("trusted_domains", cfg)
|
|
self.assertNotIn("ssrf", cfg)
|
|
|
|
def test_ssh_shape(self):
|
|
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
|
|
self.assertIn("github.com", cfg["trusted_domains"])
|
|
self.assertNotIn("100.78.141.42", cfg["trusted_domains"])
|
|
self.assertIn("100.78.141.42/32", cfg["ssrf"]["ip_allowlist"])
|
|
# Strict mode: IPv4 host is also in the api_allowlist union.
|
|
self.assertIn("100.78.141.42", cfg["api_allowlist"])
|
|
|
|
|
|
class TestRenderAndWrite(unittest.TestCase):
|
|
def setUp(self):
|
|
self.out_dir = Path(tempfile.mkdtemp())
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.out_dir, ignore_errors=True)
|
|
|
|
def test_render_emits_required_top_level_keys(self):
|
|
"""One render-level smoke check: the serialized YAML is plausibly
|
|
the shape pipelock expects. We don't grep every key here — that's
|
|
what TestBuildConfig is for."""
|
|
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
|
|
text = pipelock_render_yaml(cfg)
|
|
for required in (
|
|
"api_allowlist:",
|
|
"forward_proxy:",
|
|
"trusted_domains:",
|
|
"ssrf:",
|
|
"dlp:",
|
|
):
|
|
self.assertIn(required, text)
|
|
|
|
def test_prepare_writes_file_at_mode_600(self):
|
|
yaml_path = self.out_dir / "min.yaml"
|
|
DockerPipelockProxy().prepare(
|
|
fixture_minimal().bottles["dev"], "demo", yaml_path
|
|
)
|
|
self.assertEqual(0o600, os.stat(yaml_path).st_mode & 0o777)
|
|
|
|
def test_prepare_does_not_leak_env_names_or_values(self):
|
|
manifest = Manifest.from_json_obj({
|
|
"bottles": {
|
|
"dev": {
|
|
"env": {
|
|
"MY_SECRET": "literal-value-should-not-appear",
|
|
"ANOTHER": "?prompt-message",
|
|
},
|
|
"egress": {"allowlist": ["github.com"]},
|
|
}
|
|
},
|
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
|
})
|
|
yaml_path = self.out_dir / "secret.yaml"
|
|
DockerPipelockProxy().prepare(manifest.bottles["dev"], "demo", yaml_path)
|
|
content = yaml_path.read_text()
|
|
self.assertNotIn("literal-value-should-not-appear", content)
|
|
self.assertNotIn("MY_SECRET", content)
|
|
self.assertNotIn("prompt-message", content)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|