Skip to content

Commit ef7cd5e

Browse files
aboucaudclaude
andcommitted
docs: add design spec for harness-decoupling feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e3826af commit ef7cd5e

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Design Spec: Decouple Harness Installation from Container Image
2+
3+
**Date:** 2026-05-06
4+
**Branch:** sandboxing-execution
5+
**Status:** Implemented
6+
7+
---
8+
9+
## Problem
10+
11+
The original `lc launch claude` command shipped a single `claude-env.Containerfile` that bundled both system tooling (Apptainer, buildah, Node.js, uv) and the Claude Code CLI into one published image. This caused two problems:
12+
13+
1. **Licensing**: Publishing an image containing Claude Code (a proprietary CLI) raises redistribution concerns.
14+
2. **Extensibility**: Supporting a second harness (Mistral Vibe, OpenCode) required a separate published image per harness, bloating the registry.
15+
16+
---
17+
18+
## Solution
19+
20+
Publish one neutral `lightcone-sandbox` base image (system tools only). On first `lc launch <harness>`, install the harness inside a running container and commit the result as a local image (`lightcone-<harness>:<version>`). Subsequent launches detect the committed image and skip straight to exec.
21+
22+
---
23+
24+
## Architecture
25+
26+
### Base Image (`lightcone-sandbox.Containerfile`)
27+
28+
Renamed from `claude-env.Containerfile`. Key changes:
29+
- Removed: `npm install -g @anthropic-ai/claude-code` and `ENTRYPOINT ["claude"]`
30+
- Added: `unzip` to apt-get (required by OpenCode install script)
31+
- Changed: `ENTRYPOINT ["bash"]`
32+
- Kept: Python 3.12-slim-bookworm, FUSE, Apptainer 1.4.0, buildah, Node.js LTS, uv, lightcone-cli, `LIGHTCONE_CONTAINER=1`
33+
34+
Published as: `ghcr.io/lightconeresearch/lightcone-sandbox:<version>`
35+
No harness-specific images are published.
36+
37+
### LaunchTarget Dataclass
38+
39+
Two new fields added to `LaunchTarget` (frozen dataclass in `launcher.py`):
40+
41+
```python
42+
install_cmds: list[str] # shell commands joined with " && " and run via sh -c
43+
committed_tag_prefix: str # e.g. "lightcone-claude" → tag "lightcone-claude:<lc_version>"
44+
```
45+
46+
The `entrypoint` field (pre-existing) now carries the full binary + args, e.g. `["claude", "--dangerously-skip-permissions"]`. The launcher passes `--entrypoint <binary>` before the image tag and appends remaining args after.
47+
48+
### Harness Targets
49+
50+
All three harnesses share the `lightcone-sandbox.Containerfile` as their base and `registry_name="lightcone-sandbox"` for GHCR pull. The constant `_SANDBOX_IMAGE_NAME = "lightcone-sandbox"` is used throughout.
51+
52+
| Field | claude | mistral-vibe | opencode |
53+
|---|---|---|---|
54+
| `install_cmds` | `npm install -g @anthropic-ai/claude-code` | `uv tool install mistral-vibe` | `npm install -g opencode-ai` |
55+
| `committed_tag_prefix` | `lightcone-claude` | `lightcone-mistral-vibe` | `lightcone-opencode` |
56+
| `entrypoint` | `["claude", "--dangerously-skip-permissions"]` | `["vibe"]` | `["opencode"]` |
57+
| `env_passthrough` | `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`, `HOME`, `TERM` | `MISTRAL_API_KEY` | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY` |
58+
| `run_as_host_user` | `True` | `False` | `False` |
59+
60+
### Home Mounts (Granular Strategy)
61+
62+
`home_mounts` is an explicit list of sub-paths of `$HOME` to bind-mount. Trailing `/` = directory, others = file. Missing host paths are auto-created before mounting via `_ensure_host_path()`.
63+
64+
**Excluded** (never mounted — sensitive or ephemeral):
65+
- claude: `.claude/projects/`, `.claude/logs/`, `.claude/statsig/`
66+
- mistral-vibe: `.vibe/logs/`, `.vibe/.env`
67+
- opencode: `.local/share/opencode/storage/`
68+
69+
### `_ensure_harness_image()` Flow
70+
71+
```
72+
committed_tag = f"{target.committed_tag_prefix}:{lc_version}"
73+
74+
if not reinstall and image_exists_locally(committed_tag):
75+
return committed_tag # fast path: second+ launch
76+
77+
if reinstall:
78+
rmi committed_tag # remove old image to avoid dangling layers
79+
80+
tmp = "lc-install-<name>-<uuid8>"
81+
try:
82+
docker run --name tmp base_image sh -c install_cmd
83+
docker commit tmp committed_tag (capture_output=True)
84+
except CalledProcessError:
85+
raise ContainerBuildError(...)
86+
finally:
87+
docker rm -f tmp (check=False, capture_output=True)
88+
89+
return committed_tag
90+
```
91+
92+
`launch_target()` calls `_ensure_harness_image()` after loading the base image and before `_exec_interactive()`. The returned committed tag replaces the base image tag for both the exec and the tracking tag.
93+
94+
### `--reinstall` Flag
95+
96+
```
97+
lc launch claude --reinstall
98+
```
99+
100+
Forces re-installation: removes the existing committed image (to avoid dangling layer accumulation), then runs install again and commits a fresh image.
101+
102+
---
103+
104+
## Files Changed
105+
106+
| File | Change |
107+
|---|---|
108+
| `claude/lightcone/containers/lightcone-sandbox.Containerfile` | Renamed from `claude-env.Containerfile`; stripped harness install and ENTRYPOINT; added `unzip` |
109+
| `src/lightcone/engine/launcher.py` | Added `install_cmds`, `committed_tag_prefix` to `LaunchTarget`; added `_SANDBOX_IMAGE_NAME`, `_image_exists()`, `_ensure_host_path()`, `_ensure_harness_image()`; defined 3 harness targets; updated `launch_target()` and `_exec_interactive()` |
110+
| `src/lightcone/cli/commands.py` | Added `--reinstall` flag; updated `lc launch` help text |
111+
| `tests/test_launcher.py` | Tests for all new helpers and harness targets; `TestLaunchTargetEnsureHarness` for end-to-end wiring |
112+
| `tests/test_cli.py` | `test_launch_reinstall_forwarded_to_launch_target` |
113+
114+
---
115+
116+
## Key Invariants
117+
118+
- The base `lightcone-sandbox` image contains no proprietary software — safe to publish.
119+
- Harness images are local-only (never pushed); they are rebuilt by `--reinstall` or when the committed tag is absent.
120+
- Sensitive config paths (logs, session history, API key files) are never mounted into the container.
121+
- `_ensure_harness_image` always cleans up the temp container (via `finally`), even on install failure.
122+
- `CalledProcessError` from subprocess is always wrapped as `ContainerBuildError` — consistent with the rest of the launcher module.

0 commit comments

Comments
 (0)