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.
- project_sandbox-0.1.2/.gitignore +22 -0
- project_sandbox-0.1.2/LICENSE +21 -0
- project_sandbox-0.1.2/PKG-INFO +96 -0
- project_sandbox-0.1.2/README.md +86 -0
- project_sandbox-0.1.2/pyproject.toml +43 -0
- project_sandbox-0.1.2/src/project_sandbox/__init__.py +10 -0
- project_sandbox-0.1.2/src/project_sandbox/__main__.py +3 -0
- project_sandbox-0.1.2/src/project_sandbox/build_cache.py +78 -0
- project_sandbox-0.1.2/src/project_sandbox/chroot.py +12 -0
- project_sandbox-0.1.2/src/project_sandbox/cli.py +1680 -0
- project_sandbox-0.1.2/src/project_sandbox/config_agents.py +577 -0
- project_sandbox-0.1.2/src/project_sandbox/container_cli.py +481 -0
- project_sandbox-0.1.2/src/project_sandbox/devcontainer.py +288 -0
- project_sandbox-0.1.2/src/project_sandbox/dockerfile.py +826 -0
- project_sandbox-0.1.2/src/project_sandbox/dockerfile_checksum.py +90 -0
- project_sandbox-0.1.2/src/project_sandbox/firewall.py +75 -0
- project_sandbox-0.1.2/src/project_sandbox/git_identity.py +19 -0
- project_sandbox-0.1.2/src/project_sandbox/jj_workspace.py +330 -0
- project_sandbox-0.1.2/src/project_sandbox/oauth_refresh.py +120 -0
- project_sandbox-0.1.2/src/project_sandbox/ollama_network.py +232 -0
- project_sandbox-0.1.2/src/project_sandbox/paths.py +62 -0
- project_sandbox-0.1.2/src/project_sandbox/session.py +167 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/Dockerfile.j2 +137 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/_provision.sh.j2 +58 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/chroot-run.sh.j2 +59 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/devcontainer-entrypoint.sh.j2 +11 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/entrypoint.sh.j2 +179 -0
- project_sandbox-0.1.2/src/project_sandbox/templates/init-firewall.sh.j2 +262 -0
- project_sandbox-0.1.2/src/project_sandbox/templating.py +18 -0
- project_sandbox-0.1.2/src/project_sandbox/token_expiry.py +149 -0
- project_sandbox-0.1.2/src/project_sandbox/transcript.py +307 -0
- 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
|
+
[](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
|
+
[](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,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
|