64a31a382b
Adds pyrightconfig.json (strict, Python 3.11) covering cli.py, claude_bottle/, and tests/. Fixes the 49 strict-mode errors: - Type DockerBottle.teardown as Callable[[], None]. - ResolvedEnv default_factory uses parameterized list[str] / dict[str, str]. - Erase BottleBackend generics at the registry boundary (BottleBackend[Any, Any]) since selection is runtime-driven and callers use the unparameterized interface. - DockerBottleBackend.launch returns Generator[DockerBottle, None, None]; @contextmanager now flags Iterator returns as deprecated. - Sidestep cli.list submodule shadowing builtins.list in main()'s argv annotation via an aliased re-import in cli/__init__.py. - Cast cfg[...] results in test_pipelock_yaml at the dict[str, object] boundary. - Annotate write_fixture's fn parameter and _manifest_with_runtime's return type.
102 lines
3.8 KiB
Python
102 lines
3.8 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 typing import Any, cast
|
|
|
|
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", cast(list[str], cfg["api_allowlist"]))
|
|
self.assertIn("raw.githubusercontent.com", cast(list[str], 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", cast(list[str], cfg["trusted_domains"]))
|
|
self.assertNotIn("100.78.141.42", cast(list[str], cfg["trusted_domains"]))
|
|
self.assertIn(
|
|
"100.78.141.42/32",
|
|
cast(dict[str, Any], cfg["ssrf"])["ip_allowlist"],
|
|
)
|
|
# Strict mode: IPv4 host is also in the api_allowlist union.
|
|
self.assertIn("100.78.141.42", cast(list[str], 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):
|
|
plan = DockerPipelockProxy().prepare(
|
|
fixture_minimal().bottles["dev"], "demo", self.out_dir
|
|
)
|
|
self.assertEqual(0o600, os.stat(plan.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"}},
|
|
})
|
|
plan = DockerPipelockProxy().prepare(
|
|
manifest.bottles["dev"], "demo", self.out_dir
|
|
)
|
|
content = plan.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()
|