codefreedom 0.0.1__tar.gz → 0.0.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.
- {codefreedom-0.0.1 → codefreedom-0.0.2}/PKG-INFO +1 -1
- {codefreedom-0.0.1 → codefreedom-0.0.2}/pyproject.toml +1 -1
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/launcher.py +107 -131
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/PKG-INFO +1 -1
- {codefreedom-0.0.1 → codefreedom-0.0.2}/LICENSE +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/README.md +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/setup.cfg +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/__init__.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/__main__.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/__init__.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/claude.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/litellm_cli.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/main.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/proxy.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/env_loader.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/.env.example +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/.env.secrets.example +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/profiles/claude-code-profiles.json +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/profiles/claude-code-profiles.schema.json +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/config.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/docker-compose.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/anthropic-compatible.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/azure-foundry.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/deepseek.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/local.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/nvidia.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/openai-compatible.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/opencode-zen.yaml +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/profiles.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/SOURCES.txt +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/dependency_links.txt +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/entry_points.txt +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/requires.txt +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/top_level.txt +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_env_loader.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_init.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_profiles.py +0 -0
- {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_proxy.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codefreedom"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.2"
|
|
8
8
|
description = "CLI for running Claude Code through LiteLLM — local AI inference, anywhere"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""Claude Code Docker launcher -- runs Claude Code in
|
|
1
|
+
"""Claude Code Docker launcher -- runs Claude Code in ephemeral sandbox containers.
|
|
2
2
|
|
|
3
3
|
Provides the core execution engine for launching Claude Code through Docker
|
|
4
|
-
with profile-based model routing.
|
|
4
|
+
with profile-based model routing. Each sandbox session gets a fresh container
|
|
5
|
+
with a random name -- no more container-locking from shared reuse."""
|
|
5
6
|
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import os
|
|
10
|
+
import secrets
|
|
9
11
|
import shutil
|
|
10
12
|
import signal
|
|
11
13
|
import subprocess
|
|
@@ -16,7 +18,6 @@ from codefreedom.env_loader import eprint
|
|
|
16
18
|
|
|
17
19
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
19
|
-
CONTAINER_NAME = os.environ.get("CLAUDE_CODE_CONTAINER_NAME", "claude-dev-workspace")
|
|
20
21
|
REGISTRY = os.environ.get("CLAUDE_CODE_REGISTRY", "ghcr.io/nilayparikh")
|
|
21
22
|
IMAGE_NAME = os.environ.get("CLAUDE_CODE_IMAGE_NAME", "claude-code")
|
|
22
23
|
IMAGE_TAG = os.environ.get("CLAUDE_CODE_IMAGE_TAG", "latest")
|
|
@@ -25,6 +26,14 @@ TARGET_IMAGE = f"{REGISTRY}/{IMAGE_NAME}:{IMAGE_TAG}"
|
|
|
25
26
|
HOME_DIR = Path.home()
|
|
26
27
|
CODEFREEDOM_DIR = HOME_DIR / ".codefreedom"
|
|
27
28
|
|
|
29
|
+
_CONTAINER_PREFIX = "codefreedom-"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _generate_container_name() -> str:
|
|
33
|
+
"""Generate a random container name: codefreedom-XXXX (4 alphanumeric chars)."""
|
|
34
|
+
suffix = secrets.token_hex(2) # 4 hex chars
|
|
35
|
+
return f"{_CONTAINER_PREFIX}{suffix}"
|
|
36
|
+
|
|
28
37
|
|
|
29
38
|
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
30
39
|
|
|
@@ -68,110 +77,79 @@ def _forward_signal(
|
|
|
68
77
|
|
|
69
78
|
|
|
70
79
|
def status() -> int:
|
|
71
|
-
"""Show
|
|
80
|
+
"""Show all codefreedom sandbox containers. Returns exit code."""
|
|
72
81
|
try:
|
|
73
|
-
|
|
74
|
-
[
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[
|
|
84
|
+
"docker",
|
|
85
|
+
"ps",
|
|
86
|
+
"-a",
|
|
87
|
+
"--filter",
|
|
88
|
+
f"name={_CONTAINER_PREFIX}",
|
|
89
|
+
"--format",
|
|
90
|
+
"{{.Names}}\t{{.Status}}\t{{.CreatedAt}}",
|
|
91
|
+
],
|
|
82
92
|
capture_output=True,
|
|
83
93
|
text=True,
|
|
84
94
|
timeout=10,
|
|
85
95
|
check=False,
|
|
86
96
|
)
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"{{.Status}}",
|
|
97
|
-
],
|
|
98
|
-
capture_output=True,
|
|
99
|
-
text=True,
|
|
100
|
-
timeout=5,
|
|
101
|
-
check=False,
|
|
102
|
-
)
|
|
103
|
-
status_line = info.stdout.strip() if info.returncode == 0 else "running"
|
|
104
|
-
|
|
105
|
-
sessions = subprocess.run(
|
|
106
|
-
["docker", "top", CONTAINER_NAME, "-eo", "comm"],
|
|
107
|
-
capture_output=True,
|
|
108
|
-
text=True,
|
|
109
|
-
timeout=5,
|
|
110
|
-
check=False,
|
|
111
|
-
)
|
|
112
|
-
session_count = (
|
|
113
|
-
sessions.stdout.count("claude") if sessions.returncode == 0 else "?"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
eprint(f"[STATUS] Container '{CONTAINER_NAME}' is running.")
|
|
117
|
-
eprint(f" Status: {status_line}")
|
|
118
|
-
eprint(f" Active Claude sessions: {session_count}")
|
|
119
|
-
eprint("\n Attach a new session: codefreedom claude")
|
|
120
|
-
eprint(" Stop the container: codefreedom claude --stop")
|
|
121
|
-
elif exists.stdout.strip():
|
|
122
|
-
eprint(f"[STATUS] Container '{CONTAINER_NAME}' exists but is stopped.")
|
|
123
|
-
eprint(" Restart it: codefreedom claude")
|
|
124
|
-
eprint(" Remove it: codefreedom claude --stop")
|
|
98
|
+
containers = [l for l in result.stdout.strip().split("\n") if l]
|
|
99
|
+
if containers:
|
|
100
|
+
eprint(f"[STATUS] {len(containers)} codefreedom sandbox container(s):")
|
|
101
|
+
for line in containers:
|
|
102
|
+
name, status_line, _created = line.split("\t", 2)
|
|
103
|
+
marker = "[RUNNING]" if "Up " in status_line else "[STOPPED]"
|
|
104
|
+
eprint(f" {marker} {name} ({status_line})")
|
|
105
|
+
eprint("\n Stop all: codefreedom claude --stop")
|
|
125
106
|
else:
|
|
126
|
-
eprint(
|
|
127
|
-
eprint(" Create one: codefreedom claude")
|
|
107
|
+
eprint("[STATUS] No codefreedom sandbox containers found.")
|
|
128
108
|
return 0
|
|
129
109
|
except subprocess.TimeoutExpired:
|
|
130
110
|
eprint("[STATUS] Docker command timed out. Is Docker running?")
|
|
131
111
|
return 1
|
|
132
112
|
except FileNotFoundError:
|
|
133
|
-
eprint("[ERROR] Docker not found.
|
|
113
|
+
eprint("[ERROR] Docker not found.")
|
|
134
114
|
return 1
|
|
135
115
|
|
|
136
116
|
|
|
137
117
|
def stop() -> int:
|
|
138
|
-
"""Stop and remove
|
|
118
|
+
"""Stop and remove all codefreedom sandbox containers. Returns exit code."""
|
|
139
119
|
try:
|
|
140
|
-
|
|
141
|
-
[
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
[
|
|
122
|
+
"docker",
|
|
123
|
+
"ps",
|
|
124
|
+
"-aq",
|
|
125
|
+
"--filter",
|
|
126
|
+
f"name={_CONTAINER_PREFIX}",
|
|
127
|
+
],
|
|
142
128
|
capture_output=True,
|
|
143
129
|
text=True,
|
|
144
130
|
timeout=10,
|
|
145
131
|
check=False,
|
|
146
132
|
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
exists = subprocess.run(
|
|
158
|
-
["docker", "ps", "-aq", "-f", f"name=^{CONTAINER_NAME}$"],
|
|
133
|
+
|
|
134
|
+
ids = [c for c in result.stdout.strip().split("\n") if c]
|
|
135
|
+
if not ids:
|
|
136
|
+
eprint("[CLEAN] No codefreedom sandbox containers to remove.")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
eprint(f"[CLEAN] Stopping {len(ids)} container(s)...")
|
|
140
|
+
subprocess.run(
|
|
141
|
+
["docker", "stop"] + ids,
|
|
159
142
|
capture_output=True,
|
|
160
|
-
|
|
161
|
-
timeout=10,
|
|
143
|
+
timeout=30,
|
|
162
144
|
check=False,
|
|
163
145
|
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
eprint(" [OK] Container removed.")
|
|
173
|
-
else:
|
|
174
|
-
eprint("[CLEAN] No container to remove.")
|
|
146
|
+
subprocess.run(
|
|
147
|
+
["docker", "rm", "-f"] + ids,
|
|
148
|
+
capture_output=True,
|
|
149
|
+
timeout=30,
|
|
150
|
+
check=False,
|
|
151
|
+
)
|
|
152
|
+
eprint(" [OK] All sandbox containers removed.")
|
|
175
153
|
return 0
|
|
176
154
|
except subprocess.TimeoutExpired:
|
|
177
155
|
eprint("[ERROR] Docker command timed out.")
|
|
@@ -258,10 +236,15 @@ def run_docker(
|
|
|
258
236
|
profile_name: str,
|
|
259
237
|
sandbox_image: str | None = None,
|
|
260
238
|
) -> int:
|
|
261
|
-
"""Run claude inside
|
|
262
|
-
|
|
239
|
+
"""Run claude inside an ephemeral Docker container. Each session gets a fresh
|
|
240
|
+
container with a random name -- cleaned up on exit (including Ctrl+C)."""
|
|
241
|
+
|
|
263
242
|
image = sandbox_image or TARGET_IMAGE
|
|
243
|
+
container_name = _generate_container_name()
|
|
244
|
+
|
|
264
245
|
eprint(f"[IMAGE] Using sandbox image: {image}")
|
|
246
|
+
eprint(f"[CONTAINER] Name: {container_name}")
|
|
247
|
+
|
|
265
248
|
env_flags: List[str] = []
|
|
266
249
|
for key in sorted(profile_env.keys()):
|
|
267
250
|
val = profile_env[key]
|
|
@@ -303,8 +286,8 @@ def run_docker(
|
|
|
303
286
|
f"{workspace_dir / '.claude'}:/workspace/.claude",
|
|
304
287
|
]
|
|
305
288
|
|
|
306
|
-
# ──
|
|
307
|
-
eprint(f"[IMAGE]
|
|
289
|
+
# ── Ensure image is pulled ────────────────────────────────────────────
|
|
290
|
+
eprint(f"[IMAGE] Pulling '{image}'...")
|
|
308
291
|
pull = subprocess.run(
|
|
309
292
|
["docker", "pull", image],
|
|
310
293
|
capture_output=True,
|
|
@@ -318,71 +301,64 @@ def run_docker(
|
|
|
318
301
|
eprint(f" {pull.stderr.strip()}")
|
|
319
302
|
return 1
|
|
320
303
|
|
|
321
|
-
# ──
|
|
304
|
+
# ── Ensure workspace .claude dir exists ───────────────────────────────
|
|
322
305
|
(workspace_dir / ".claude").mkdir(parents=True, exist_ok=True)
|
|
323
306
|
|
|
324
|
-
# ──
|
|
325
|
-
|
|
326
|
-
|
|
307
|
+
# ── Start ephemeral container ─────────────────────────────────────────
|
|
308
|
+
eprint(f"[RUN] Creating ephemeral container '{container_name}'...")
|
|
309
|
+
create = subprocess.run(
|
|
310
|
+
["docker", "run", "-d", "--rm", "--name", container_name]
|
|
311
|
+
+ base_opts
|
|
312
|
+
+ env_flags
|
|
313
|
+
+ [image, "sleep", "infinity"],
|
|
327
314
|
capture_output=True,
|
|
328
315
|
text=True,
|
|
329
|
-
timeout=
|
|
316
|
+
timeout=60,
|
|
330
317
|
check=False,
|
|
331
318
|
)
|
|
319
|
+
if create.returncode != 0:
|
|
320
|
+
eprint("[ERROR] Failed to start container.")
|
|
321
|
+
if create.stderr:
|
|
322
|
+
eprint(f" {create.stderr.strip()}")
|
|
323
|
+
return 1
|
|
324
|
+
eprint(" [OK] Container started.")
|
|
332
325
|
|
|
333
|
-
|
|
334
|
-
stale = subprocess.run(
|
|
335
|
-
["docker", "ps", "-aq", "-f", f"name=^{CONTAINER_NAME}$"],
|
|
336
|
-
capture_output=True,
|
|
337
|
-
text=True,
|
|
338
|
-
timeout=10,
|
|
339
|
-
check=False,
|
|
340
|
-
)
|
|
341
|
-
if stale.stdout.strip():
|
|
342
|
-
eprint(f"[CLEAN] Removing stale container '{CONTAINER_NAME}'...")
|
|
343
|
-
subprocess.run(
|
|
344
|
-
["docker", "rm", "-f", CONTAINER_NAME],
|
|
345
|
-
capture_output=True,
|
|
346
|
-
timeout=30,
|
|
347
|
-
check=False,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
eprint(f"[RUN] Starting persistent container '{CONTAINER_NAME}'...")
|
|
351
|
-
create = subprocess.run(
|
|
352
|
-
["docker", "run", "-d", "--name", CONTAINER_NAME]
|
|
353
|
-
+ base_opts
|
|
354
|
-
+ env_flags
|
|
355
|
-
+ [image, "sleep", "infinity"],
|
|
356
|
-
capture_output=True,
|
|
357
|
-
text=True,
|
|
358
|
-
timeout=60,
|
|
359
|
-
check=False,
|
|
360
|
-
)
|
|
361
|
-
if create.returncode != 0:
|
|
362
|
-
eprint("[ERROR] Failed to start container.")
|
|
363
|
-
if create.stderr:
|
|
364
|
-
eprint(f" {create.stderr.strip()}")
|
|
365
|
-
return 1
|
|
366
|
-
eprint(" [OK] Container started (background).")
|
|
367
|
-
else:
|
|
368
|
-
eprint(f"[RUN] Reusing running container '{CONTAINER_NAME}'.")
|
|
369
|
-
|
|
370
|
-
# ── Step D: Exec claude into the container ─────────────────────────────
|
|
326
|
+
# ── Exec claude into the container ────────────────────────────────────
|
|
371
327
|
eprint("[EXEC] Attaching Claude Code session...")
|
|
372
328
|
|
|
373
329
|
exec_cmd = (
|
|
374
330
|
["docker", "exec", "-it"]
|
|
375
331
|
+ ["-u", f"{uid}:{gid}", "-e", f"HOME=/home/{HOME_DIR.name}"]
|
|
376
332
|
+ env_flags
|
|
377
|
-
+ [
|
|
333
|
+
+ [container_name, "claude", "--dangerously-skip-permissions"]
|
|
378
334
|
+ claude_args
|
|
379
335
|
)
|
|
380
336
|
|
|
337
|
+
exit_code = 1
|
|
381
338
|
try:
|
|
382
339
|
proc = subprocess.Popen(exec_cmd)
|
|
383
340
|
signal.signal(signal.SIGINT, lambda s, f: _forward_signal(proc, s, f))
|
|
384
341
|
signal.signal(signal.SIGTERM, lambda s, f: _forward_signal(proc, s, f))
|
|
385
342
|
proc.wait()
|
|
386
|
-
|
|
343
|
+
exit_code = proc.returncode
|
|
387
344
|
except KeyboardInterrupt:
|
|
388
|
-
|
|
345
|
+
exit_code = 130
|
|
346
|
+
finally:
|
|
347
|
+
# ── Clean up the ephemeral container ──────────────────────────
|
|
348
|
+
eprint(f"[CLEAN] Stopping container '{container_name}'...")
|
|
349
|
+
subprocess.run(
|
|
350
|
+
["docker", "stop", container_name],
|
|
351
|
+
capture_output=True,
|
|
352
|
+
timeout=15,
|
|
353
|
+
check=False,
|
|
354
|
+
)
|
|
355
|
+
# --rm flag handles auto-removal; force-remove as fallback
|
|
356
|
+
subprocess.run(
|
|
357
|
+
["docker", "rm", "-f", container_name],
|
|
358
|
+
capture_output=True,
|
|
359
|
+
timeout=5,
|
|
360
|
+
check=False,
|
|
361
|
+
)
|
|
362
|
+
eprint(" [OK] Container cleaned up.")
|
|
363
|
+
|
|
364
|
+
return exit_code
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/profiles/claude-code-profiles.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/azure-foundry.yaml
RENAMED
|
File without changes
|
{codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/deepseek.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/nvidia.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/opencode-zen.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|