project-sandbox 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. project_sandbox-0.1.2/.gitignore +22 -0
  2. project_sandbox-0.1.2/LICENSE +21 -0
  3. project_sandbox-0.1.2/PKG-INFO +96 -0
  4. project_sandbox-0.1.2/README.md +86 -0
  5. project_sandbox-0.1.2/pyproject.toml +43 -0
  6. project_sandbox-0.1.2/src/project_sandbox/__init__.py +10 -0
  7. project_sandbox-0.1.2/src/project_sandbox/__main__.py +3 -0
  8. project_sandbox-0.1.2/src/project_sandbox/build_cache.py +78 -0
  9. project_sandbox-0.1.2/src/project_sandbox/chroot.py +12 -0
  10. project_sandbox-0.1.2/src/project_sandbox/cli.py +1680 -0
  11. project_sandbox-0.1.2/src/project_sandbox/config_agents.py +577 -0
  12. project_sandbox-0.1.2/src/project_sandbox/container_cli.py +481 -0
  13. project_sandbox-0.1.2/src/project_sandbox/devcontainer.py +288 -0
  14. project_sandbox-0.1.2/src/project_sandbox/dockerfile.py +826 -0
  15. project_sandbox-0.1.2/src/project_sandbox/dockerfile_checksum.py +90 -0
  16. project_sandbox-0.1.2/src/project_sandbox/firewall.py +75 -0
  17. project_sandbox-0.1.2/src/project_sandbox/git_identity.py +19 -0
  18. project_sandbox-0.1.2/src/project_sandbox/jj_workspace.py +330 -0
  19. project_sandbox-0.1.2/src/project_sandbox/oauth_refresh.py +120 -0
  20. project_sandbox-0.1.2/src/project_sandbox/ollama_network.py +232 -0
  21. project_sandbox-0.1.2/src/project_sandbox/paths.py +62 -0
  22. project_sandbox-0.1.2/src/project_sandbox/session.py +167 -0
  23. project_sandbox-0.1.2/src/project_sandbox/templates/Dockerfile.j2 +137 -0
  24. project_sandbox-0.1.2/src/project_sandbox/templates/_provision.sh.j2 +58 -0
  25. project_sandbox-0.1.2/src/project_sandbox/templates/chroot-run.sh.j2 +59 -0
  26. project_sandbox-0.1.2/src/project_sandbox/templates/devcontainer-entrypoint.sh.j2 +11 -0
  27. project_sandbox-0.1.2/src/project_sandbox/templates/entrypoint.sh.j2 +179 -0
  28. project_sandbox-0.1.2/src/project_sandbox/templates/init-firewall.sh.j2 +262 -0
  29. project_sandbox-0.1.2/src/project_sandbox/templating.py +18 -0
  30. project_sandbox-0.1.2/src/project_sandbox/token_expiry.py +149 -0
  31. project_sandbox-0.1.2/src/project_sandbox/transcript.py +307 -0
  32. project_sandbox-0.1.2/src/project_sandbox/worktree.py +174 -0
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ *.egg-info/
6
+ .dist/
7
+ build/
8
+ dist/
9
+ .venv/
10
+ .project-sandbox/
11
+ .uv-cache
12
+ .uv_cache
13
+ .pycache-test
14
+ .pytest_cache
15
+ .uv-python
16
+ .claude/worktrees
17
+ .agents
18
+ .claude/skills
19
+ # project-sandbox — do not commit agent secrets
20
+ .devcontainer/
21
+ # release state tracking
22
+ .release-status/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Peter Krusche, 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: project-sandbox
3
+ Version: 0.1.2
4
+ Summary: Sandbox coding agents inside isolated container runtimes.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: jinja2==3.1.6
9
+ Description-Content-Type: text/markdown
10
+
11
+ # project-sandbox
12
+
13
+ [![PyPI](https://img.shields.io/pypi/v/project-sandbox.svg)](https://pypi.org/project/project-sandbox/)
14
+
15
+ `project-sandbox` runs Claude Code, Codex CLI, OpenCode, Pi, or a plain Bash shell
16
+ inside per-project Linux containers. On macOS, direct CLI runs default to Apple's
17
+ [`container`](https://github.com/apple/container) runtime, where each container
18
+ runs in its own VM. On Linux, direct CLI runs support Docker or Podman.
19
+
20
+ > ⚠️ Created with the help of AI. \
21
+ > 🚧 Experimental work in progress \
22
+ > ‼️ Use at your own risk.
23
+
24
+ ## Main features
25
+
26
+ Many sandboxes exist - this is the one with a feature-set / configureable agency-boundary that I was comfortable with in the end:
27
+
28
+ * **Strong isolation** - on OSX with Apple Container VMs. Custom Dockerfile support (as long as it's based on Debian)
29
+ * **Agent config glue**: Forward host credentials / agent subscriptions into containers selectively, update settings to bypass permissions inside the container.
30
+ * **Devcontainer support**: Creates a matched devcontainer config for editor support (weaker isolation but integrated workflow).
31
+ * **Unsupervised job runs** - submit batch jobs.
32
+ * **Network access restrictions**: restrict to allowed domains, (somewhat) hardened firewall script.
33
+ * **git/jj integration**: Managed execution with worktrees / workspaces. No credentials to push inside containers.
34
+ * **pinned dependencies**: pre-install agents & extra tools into the image, manual upversioning.
35
+ * **Simple workflow** (in my view).
36
+ * **Minimal dependencies** (jinja2)
37
+
38
+ ## Quick Start
39
+
40
+ Install from PyPI:
41
+
42
+ ```bash
43
+ uv tool install project-sandbox
44
+ project-sandbox --help
45
+ ```
46
+
47
+ Or run directly from PyPI without installing:
48
+
49
+ ```bash
50
+ uvx project-sandbox --help
51
+ uvx project-sandbox /absolute/path/to/repo python:3.12-slim
52
+ ```
53
+
54
+ From a source checkout:
55
+
56
+ ```bash
57
+ uv sync
58
+ uv run project-sandbox --help
59
+ ```
60
+
61
+ Generate sandbox files for a project:
62
+
63
+ ```bash
64
+ project-sandbox /absolute/path/to/repo python:3.12-slim
65
+ ```
66
+
67
+ Preview every action without writing files or starting a runtime:
68
+
69
+ ```bash
70
+ project-sandbox --dry-run /absolute/path/to/repo python:3.12-slim
71
+ ```
72
+
73
+ Start an agent in the sandbox:
74
+
75
+ ```bash
76
+ project-sandbox --agent codex /absolute/path/to/repo python:3.12-slim
77
+ ```
78
+
79
+ Build on top of an existing project Dockerfile:
80
+
81
+ ```bash
82
+ project-sandbox /absolute/path/to/repo --dockerfile /absolute/path/to/repo/Dockerfile
83
+ ```
84
+
85
+ ## Documentation
86
+
87
+ - [Documentation index](https://github.com/pkrusche/project-sandbox/tree/main/docs)
88
+ - [Usage guide](https://github.com/pkrusche/project-sandbox/blob/main/docs/usage.md)
89
+ - [Generated files and runtime behavior](https://github.com/pkrusche/project-sandbox/blob/main/docs/runtime.md)
90
+ - [Security model](https://github.com/pkrusche/project-sandbox/blob/main/docs/security.md)
91
+ - [Development guide](https://github.com/pkrusche/project-sandbox/blob/main/docs/development.md)
92
+ - [References and related projects](https://github.com/pkrusche/project-sandbox/blob/main/docs/references.md)
93
+
94
+ ## License
95
+
96
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,86 @@
1
+ # project-sandbox
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/project-sandbox.svg)](https://pypi.org/project/project-sandbox/)
4
+
5
+ `project-sandbox` runs Claude Code, Codex CLI, OpenCode, Pi, or a plain Bash shell
6
+ inside per-project Linux containers. On macOS, direct CLI runs default to Apple's
7
+ [`container`](https://github.com/apple/container) runtime, where each container
8
+ runs in its own VM. On Linux, direct CLI runs support Docker or Podman.
9
+
10
+ > ⚠️ Created with the help of AI. \
11
+ > 🚧 Experimental work in progress \
12
+ > ‼️ Use at your own risk.
13
+
14
+ ## Main features
15
+
16
+ Many sandboxes exist - this is the one with a feature-set / configureable agency-boundary that I was comfortable with in the end:
17
+
18
+ * **Strong isolation** - on OSX with Apple Container VMs. Custom Dockerfile support (as long as it's based on Debian)
19
+ * **Agent config glue**: Forward host credentials / agent subscriptions into containers selectively, update settings to bypass permissions inside the container.
20
+ * **Devcontainer support**: Creates a matched devcontainer config for editor support (weaker isolation but integrated workflow).
21
+ * **Unsupervised job runs** - submit batch jobs.
22
+ * **Network access restrictions**: restrict to allowed domains, (somewhat) hardened firewall script.
23
+ * **git/jj integration**: Managed execution with worktrees / workspaces. No credentials to push inside containers.
24
+ * **pinned dependencies**: pre-install agents & extra tools into the image, manual upversioning.
25
+ * **Simple workflow** (in my view).
26
+ * **Minimal dependencies** (jinja2)
27
+
28
+ ## Quick Start
29
+
30
+ Install from PyPI:
31
+
32
+ ```bash
33
+ uv tool install project-sandbox
34
+ project-sandbox --help
35
+ ```
36
+
37
+ Or run directly from PyPI without installing:
38
+
39
+ ```bash
40
+ uvx project-sandbox --help
41
+ uvx project-sandbox /absolute/path/to/repo python:3.12-slim
42
+ ```
43
+
44
+ From a source checkout:
45
+
46
+ ```bash
47
+ uv sync
48
+ uv run project-sandbox --help
49
+ ```
50
+
51
+ Generate sandbox files for a project:
52
+
53
+ ```bash
54
+ project-sandbox /absolute/path/to/repo python:3.12-slim
55
+ ```
56
+
57
+ Preview every action without writing files or starting a runtime:
58
+
59
+ ```bash
60
+ project-sandbox --dry-run /absolute/path/to/repo python:3.12-slim
61
+ ```
62
+
63
+ Start an agent in the sandbox:
64
+
65
+ ```bash
66
+ project-sandbox --agent codex /absolute/path/to/repo python:3.12-slim
67
+ ```
68
+
69
+ Build on top of an existing project Dockerfile:
70
+
71
+ ```bash
72
+ project-sandbox /absolute/path/to/repo --dockerfile /absolute/path/to/repo/Dockerfile
73
+ ```
74
+
75
+ ## Documentation
76
+
77
+ - [Documentation index](https://github.com/pkrusche/project-sandbox/tree/main/docs)
78
+ - [Usage guide](https://github.com/pkrusche/project-sandbox/blob/main/docs/usage.md)
79
+ - [Generated files and runtime behavior](https://github.com/pkrusche/project-sandbox/blob/main/docs/runtime.md)
80
+ - [Security model](https://github.com/pkrusche/project-sandbox/blob/main/docs/security.md)
81
+ - [Development guide](https://github.com/pkrusche/project-sandbox/blob/main/docs/development.md)
82
+ - [References and related projects](https://github.com/pkrusche/project-sandbox/blob/main/docs/references.md)
83
+
84
+ ## License
85
+
86
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "project-sandbox"
7
+ version = "0.1.2"
8
+ description = "Sandbox coding agents inside isolated container runtimes."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "jinja2==3.1.6",
14
+ ]
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest==9.1.1",
19
+ "ruff==0.16.1",
20
+ ]
21
+
22
+ [project.scripts]
23
+ project-sandbox = "project_sandbox.cli:main"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/project_sandbox"]
27
+
28
+ [tool.hatch.build.targets.sdist]
29
+ include = [
30
+ "/src/project_sandbox",
31
+ "/LICENSE",
32
+ "/README.md",
33
+ "/pyproject.toml",
34
+ ]
35
+
36
+ [tool.uv]
37
+ cache-dir = ".uv_cache"
38
+
39
+ [tool.ruff.lint.per-file-ignores]
40
+ # Registry payload validation reports malformed external data, not bad caller types.
41
+ "scripts/update-pins.py" = ["TRY004"]
42
+ # Keeping resource-owning contexts nested makes their cleanup boundaries explicit.
43
+ "tests/*.py" = ["SIM117"]
@@ -0,0 +1,10 @@
1
+ """project-sandbox package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ try:
8
+ __version__ = version("project-sandbox")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,78 @@
1
+ """Input fingerprinting so a matching image can be reused without rebuilding.
2
+
3
+ The CLI rebuilds the container image on every run by default. When the build
4
+ inputs have not changed and the image still exists, that work is wasted. This
5
+ module computes a deterministic fingerprint of the build inputs and records it,
6
+ alongside the image tag, in a sidecar JSON file under the generated
7
+ ``.project-sandbox`` directory. The caller compares the current fingerprint with
8
+ the recorded one (and confirms the image still exists) to decide whether the
9
+ build can be skipped.
10
+
11
+ Skipping is correctness-safe: it only happens on an exact fingerprint+tag match,
12
+ and any read/parse problem degrades to "not cached" so the build runs.
13
+ """
14
+
15
+ import hashlib
16
+ import json
17
+ from pathlib import Path
18
+
19
+ # Top-level files in the generated context that fully determine the CLI image.
20
+ # The CLI always builds context_dir/"Dockerfile"; entrypoint.sh, init-firewall.sh
21
+ # and project-sandbox-devcontainer-init are COPY'd into it.
22
+ _BUILD_INPUT_FILES = (
23
+ "Dockerfile",
24
+ "entrypoint.sh",
25
+ "init-firewall.sh",
26
+ "project-sandbox-devcontainer-init",
27
+ )
28
+
29
+ _STATE_FILENAME = ".build-state.json"
30
+
31
+
32
+ def state_path(context_dir: Path) -> Path:
33
+ return context_dir / _STATE_FILENAME
34
+
35
+
36
+ def compute_fingerprint(context_dir: Path, *, extra: dict[str, str]) -> str:
37
+ """Return a SHA256 over the build-input files plus the ``extra`` mapping.
38
+
39
+ ``extra`` carries inputs that are not files in ``context_dir`` (the resolved
40
+ base image and the image tag). Missing input files are simply skipped, so
41
+ the fingerprint still reflects which files are present.
42
+ """
43
+ h = hashlib.sha256()
44
+ for name in sorted(_BUILD_INPUT_FILES):
45
+ path = context_dir / name
46
+ if not path.is_file():
47
+ continue
48
+ h.update(name.encode("utf-8"))
49
+ h.update(b"\0")
50
+ h.update(hashlib.sha256(path.read_bytes()).hexdigest().encode("ascii"))
51
+ h.update(b"\0")
52
+ h.update(b"extra\0")
53
+ h.update(json.dumps(extra, sort_keys=True, separators=(",", ":")).encode("utf-8"))
54
+ return h.hexdigest()
55
+
56
+
57
+ def read_state(context_dir: Path) -> dict | None:
58
+ try:
59
+ data = json.loads(state_path(context_dir).read_text(encoding="utf-8"))
60
+ except (OSError, ValueError):
61
+ return None
62
+ return data if isinstance(data, dict) else None
63
+
64
+
65
+ def write_state(context_dir: Path, *, image_tag: str, fingerprint: str) -> None:
66
+ state_path(context_dir).write_text(
67
+ json.dumps({"image_tag": image_tag, "fingerprint": fingerprint}) + "\n",
68
+ encoding="utf-8",
69
+ )
70
+
71
+
72
+ def is_cache_valid(context_dir: Path, *, image_tag: str, fingerprint: str) -> bool:
73
+ state = read_state(context_dir)
74
+ return (
75
+ state is not None
76
+ and state.get("image_tag") == image_tag
77
+ and state.get("fingerprint") == fingerprint
78
+ )
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+
3
+ from . import templating
4
+
5
+
6
+ def render(context_dir: Path) -> Path:
7
+ out = context_dir / "chroot-run.sh"
8
+ out.write_text(
9
+ templating.get_template("chroot-run.sh.j2").render() + "\n", encoding="utf-8"
10
+ )
11
+ out.chmod(0o700)
12
+ return out