Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ forwarding, and exit codes.
Run in any project directory:

```bash
enclave # Start claude (default) in current project
enclave --tool codex # Use a different tool
enclave # Start the current project's tool (asks which one on the first run)
enclave --tool codex # Use a different tool for one run
enclave --backend qemu --tool codex # Experimental QEMU microVM run (implies --slim, all-network)
enclave continue # Continue latest session
enclave ps # List running containers (--all for stopped, --json for scripts)
Expand All @@ -198,6 +198,8 @@ enclave info # Show config and image details
enclave version # Show binary version and source commit (--json; alias: --version)
```

**Tool selection:** The first interactive session asks which coding agent to run and saves the answer as `"tool"` in `~/.config/enclave/config.json`; later runs start it without asking. Scripts and non-terminal runs are never asked: without a configured tool they fail with a message naming `--tool` and the `tool` key. See [Tool selection](docs/cli-reference.md#tool-selection).

**Authentication:** The simplest and recommended approach is to just log in from inside the container the first time you run — OAuth sessions are saved to a persistent auth store on the host and reused automatically on every subsequent run. No configuration needed.

To use declared env credentials instead, place them in `~/.local/state/enclave/secrets/global.env`. See [Authentication & Secrets](docs/auth.md).
Expand Down
13 changes: 10 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ The restricted network request flow has a separate
- **Profiles** (`extensions/tools/<tool>/spec.yaml`, `kind: sandbox`): define tool command, session continuation args (`continueArgs`, `resumeArgs`), config location, optional settings/skills metadata (`settingsFile`, `settingsTarget`, `skillsDir`), optional host passthrough allow-list (`passthroughPaths`), optional QEMU bundle minimum memory (`qemuMinMemoryMiB`) and config-store cache hint (`qemuStoreCacheMmap`), declared credential sources (`credentials.sources`) including API-key metadata, YOLO flag, and per-provider auth configuration (`providers`: credentials, auth files, auth session checks, OAuth ports).
- **Runtime assets** (`runtime-assets/gateway-allowlists/`, `runtime-assets/build-scripts/`, `runtime-assets/auth-reconcile.sh`, `runtime-assets/net.sh`): DNS allowlists, Docker weaving scripts, and shared entrypoint helpers baked into the image. Tool templates live in `extensions/tools/<tool>/templates/` and are aggregated during build.
- **Asset discovery**: `ENCLAVE_HOME` has explicit precedence, followed by a valid app root above the resolved executable path. This keeps package-managed installs on `/usr/share/enclave` and in-tree builds on live checkout files. Other binaries extract their embedded assets into an append-only `assets/<content-hash>/` directory under the platform cache root. The former unversioned data-root lookup is not used.
- **Image selection**: images are per-tool. The default tag is `enclave-<tool>:latest` for the selected `--tool` (default `claude`); `--slim` uses `enclave-<tool>:slim`. When running from a git checkout on a non-default branch, the tag is prefixed with the branch name and hash (for example, `enclave-codex:branch-<name>-<hash>-latest`) to avoid overwriting main images. `--base-image` or devcontainer mode derives a separate tag (e.g., `enclave-codex:base-<hash>-latest`) unless `--image-name` is set.
- **Tool selection** ([`internal/app/tool.go`](../internal/app/tool.go)): `--tool` selects the sandbox profile. The default is unset: the first command that needs a tool asks once which installed agent to use and saves the answer as `"tool"` in the global config. `actionNeedsTool` in [`internal/app/actions.go`](../internal/app/actions.go) decides which verbs need one; `update` with explicit tool arguments, `stop <session>` and `cleanup --all` name their own scope, and listing, status, extension and configuration verbs never read the default and leave it unresolved. The menu offers every installed agent profile and never the IDE profiles; without a terminal, under `--json` or `--yes`, or when the question goes unanswered, the command fails naming `--tool` and the `tool` key instead of guessing an agent.
- **Image selection**: images are per-tool. The default tag is `enclave-<tool>:latest` for the selected tool; `--slim` uses `enclave-<tool>:slim`. When running from a git checkout on a non-default branch, the tag is prefixed with the branch name and hash (for example, `enclave-codex:branch-<name>-<hash>-latest`) to avoid overwriting main images. `--base-image` or devcontainer mode derives a separate tag (e.g., `enclave-codex:base-<hash>-latest`) unless `--image-name` is set.
- **Agent Node isolation**: Node-based agent CLIs are installed with a private runtime at `/opt/enclave/node` and launcher shebangs are rewritten to that absolute node path. This keeps agent runtime Node independent from user/project `node` on PATH.
- **Isolation backend**: `--backend` selects the session isolation backend. The default `auto` resolves once at startup to `docker` or `podman`, whichever CLI is on `PATH` (asking once, and saving the answer to the global config, when both are). `docker` supports the full feature set. `podman` is the same backend driven through podman's Docker-compatible CLI: `internal/docker` switches its binary once at startup, and the backend adds `--userns=keep-id` to the gateway (run as root inside it) and auth-reconcile containers, while the session container joins the gateway's user namespace together with its network namespace, so the host UID/GID survives rootless podman's user namespace on bind-mounted stores and the session never joins a network namespace owned by a foreign user namespace (which fails under runc). Experimental `qemu` supports foreground slim/no-feature unrestricted sessions in an Alpine microVM bundle; it is x86-64 only (`qemu-system-x86_64`, x86-64 guest rootfs) and is KVM-accelerated only on x86-64 Linux hosts. Both backends realize persistent stores from the shared host-directory layout (`internal/backend/hoststore`), so auth, tool config, and persisted env are shared between containers and microVMs.
- **User-defined subcommands**: executables under `~/.config/enclave/commands/{host,session}/` become `enclave <name>` verbs. `cli.Parse` discovers them, registers name-only stub commands (Cobra group "User Commands") so they list in `--help` and shell completion, and intercepts a matching first positional *before* `normalizeArgs`/Cobra so the trailing line reaches the script verbatim (preserving the unknown-command rejection for everything else). enclave flags must precede the name: host commands accept only the global group, session commands accept the full session flag set. `host/` commands exec directly on the host (`os/exec`, exit code/stdin/stdout passthrough, `ENCLAVE_BIN`/`ENCLAVE_PROJECT_ROOT`/`ENCLAVE_CONFIG_DIR` injected). `session/` commands run through the normal run pipeline as a shell-style execution (`opts.Shell=true`, argv `bash -c 'exec "$@"' <name> <container-path> <args>` so the script's shebang is honored via execve).
Expand Down Expand Up @@ -285,7 +286,13 @@ The complete flow from CLI arguments to final options:
│ ├── config.ResolveToolOverrideDefaults(global, project, opts.Tool) │
│ └── config.ApplyDefaultsWithSources(..., SourceToolOverride) │
│ │
│ 7. Final Options with Sources tracked │
│ 7. Resolve the unset tool (app.resolveTool) │
│ ├── "auto" with a terminal: ask once among the installed │
│ │ agent profiles and save the answer as "tool" globally │
│ ├── otherwise: claude (scripts, --json, --yes, other verbs) │
│ └── Steps 3-6 run again for the resolved tool │
│ │
│ 8. Final Options with Sources tracked │
│ └── Each field knows: CLI, ToolOverride, Project, Global, or Default │
└─────────────────────────────────────────────────────────────────────────────┘
```
Expand Down Expand Up @@ -358,7 +365,7 @@ included explicitly.

## Image Naming

Images are per-tool; `<tool>` below is the selected `--tool` (default `claude`).
Images are per-tool; `<tool>` below is the selected tool.
Default image naming behavior:

- Explicit `--image-name`: always used (no auto-derivation).
Expand Down
14 changes: 11 additions & 3 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ unchanged. See [windows.md](windows.md).

| Command | Description |
|---------|-------------|
| `enclave` | Start a new session (default tool: claude) |
| `enclave` | Start a new session with the selected tool |
| `enclave run` | Explicit alias for the above |
| `enclave continue` | Continue the latest session for the selected tool |
| `enclave resume` | Session picker/list when supported (falls back to `continue`) |
Expand Down Expand Up @@ -202,7 +202,7 @@ Mutation commands (`add-domain`, `remove-domain`, `set-mode`) apply the new poli

| Flag | Description |
|------|-------------|
| `--tool <tool>` | Tool profile to use (`claude` by default; run `enclave tools` for the installed list) |
| `--tool <tool>` | Tool profile to use (asked once on the first interactive session, then the saved choice; run `enclave tools` for the installed list) |
| `--backend <backend>` | Isolation backend: `auto` (default: docker or podman, whichever is installed), `docker`, `podman`, or experimental `qemu` |
| `--name <name>` | Named persistent session |
| `--background` | Detached background session |
Expand Down Expand Up @@ -277,7 +277,7 @@ Mutation commands (`add-domain`, `remove-domain`, `set-mode`) apply the new poli

| Setting | Default |
|---------|---------|
| Tool | `claude` |
| Tool | Asked once on the first interactive session, then the saved choice; an error without a terminal until `--tool` or the `tool` key is set |
| Network | Restricted (allowlisted domains only) |
| Persistence | Enabled (auth, env, history host-directory stores) |
| YOLO mode | Enabled |
Expand All @@ -286,6 +286,14 @@ Mutation commands (`add-domain`, `remove-domain`, `set-mode`) apply the new poli

Persistent defaults can be set in `~/.config/enclave/config.json` (global) or `~/.config/enclave/projects/<hash>/config.json` (per-project). See [Configuration](configuration.md).

## Tool selection

The tool is unset by default. The first command that needs one asks once which coding agent to use and saves the answer as `"tool"` in `~/.config/enclave/config.json`; later runs read it from there and never ask again. A command needs a tool when it starts a session or builds its image (`run`, `shell`, `continue`, `resume`, `exec`, and `update` without explicit tool arguments) or scopes policy, stores, or containers by tool (`info`, `auth import`, `auth export`, `network …`, `cleanup` without `--all`, and `stop` without a session argument). The question is only asked when stdin, stdout, and stderr are terminals and no `--json` or `--yes` was given; without a terminal, or when the question goes unanswered, the command fails with a message naming `--tool` and the `tool` key instead of guessing an agent. Commands that never read the default tool (`ps`, `status`, `attach`, `tools`, `features`, `config`, `review-target`, `theia`, …) neither ask nor fail; they take `--tool` at most as an explicit filter.

The menu lists every installed agent. The IDE profiles (`theia`, `theia-next`) are not offered: they attach a host IDE to a container rather than running an agent in the terminal. Pick them with `--tool theia` or the `tool` key.

`--tool` overrides the saved choice for a single run, and a configured `tool` disables the question. Set `"tool": "auto"` to be asked again.

## Backend detection

The default backend is `auto`: enclave uses docker when its CLI is on `PATH`, otherwise podman. A `docker` command that is really the `podman-docker` shim counts as podman, and an explicit `docker`, from `--backend` or the `backend` key, that turns out to be the shim is driven as podman as well, with a notice. When both engines are installed, a command that uses an engine asks once which one to use and saves the answer as `"backend"` in `~/.config/enclave/config.json`. The question is only asked when stdin, stdout, and stderr are terminals and no `--json` or `--yes` was given; otherwise (scripts, captured output, JSON consumers) docker is used and a notice points at that key. Commands that never touch an engine (`tools`, `features`, `config`, `review-target`, `network print`, `network diff`, `devcontainer generate`) neither detect nor ask. When neither engine is found, the engine check reports it. An explicit `--backend` or a configured `backend` disables detection.
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ supported under `tool_overrides.<tool>`.

| Key | Description |
|-----|-------------|
| `tool` | Default tool (e.g. `claude`, `codex`) |
| `tool` | Default tool (e.g. `claude`, `codex`); unset means the first interactive session asks once and writes the answer here (see [Tool selection](cli-reference.md#tool-selection)) |
| `backend` | Isolation backend: `auto` (default) uses docker or podman, whichever is installed, and asks once when both are; or `docker`, `podman`, experimental `qemu` |
| `host_config` | `none` (default) or `passthrough` |
| `skills_validation` | Shared skill validation: `strict` (default) or `agent`; supports tool overrides |
Expand Down
17 changes: 8 additions & 9 deletions docs/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,12 @@ Session continuation arguments are declared under `sandbox`:
### Build Selection

Runtime images are per-tool: the CLI builds and runs exactly one tool image per
session, selected with `--tool <name>` (default: `claude`). Each tool gets its
session, selected with `--tool <name>` or the `tool` key. Each tool gets its
own image tagged `enclave-<tool>:...`, so rebuilding or updating one tool
never invalidates another tool's image.

```bash
enclave --rebuild # build/run the default tool (claude)
enclave --rebuild # build/run the selected tool
enclave --tool codex --rebuild # build/run the codex image
```

Expand Down Expand Up @@ -797,11 +797,10 @@ go build ./cmd/enclave
### 2. Build and Run with enclave

```bash
# Build and run with default tool (claude)
# This builds the full image with all default-enabled features
./enclave --rebuild
# Build and run the claude image with all default-enabled features
./enclave --tool claude --rebuild

# Run with a specific tool
# Run another tool
./enclave --tool codex
```

Expand Down Expand Up @@ -866,7 +865,7 @@ type direnv

```bash
# Test agents-only image (no features)
./enclave --slim --rebuild
./enclave --tool claude --slim --rebuild

# Inside container:
claude --version # Should work (tool installed)
Expand Down Expand Up @@ -923,15 +922,15 @@ docker build --progress=plain . 2>&1 | \
For production builds with per-tool layer caching, use the CLI:

```bash
enclave --rebuild
enclave --tool <name> --rebuild
```

### Quick Verification Checklist

| Test | Command | Expected |
|------|---------|----------|
| Go builds | `go build ./cmd/enclave` | No errors |
| Default run | `./enclave` | Starts claude in container |
| Claude run | `./enclave --tool claude` | Starts claude in container |
| Tool select | `./enclave --tool codex` | Starts codex |
| gh installed | (in container) `gh --version` | Shows version |
| glab installed (opt-in) | (in container) `glab --version` | Shows version when enabled |
Expand Down
6 changes: 4 additions & 2 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Built-in tool profiles:

| Tool | Description |
|------|-------------|
| `claude` | [Claude Code](https://www.anthropic.com/claude-code) (Anthropic) — default |
| `claude` | [Claude Code](https://www.anthropic.com/claude-code) (Anthropic) |
| `codex` | [Codex CLI](https://github.com/openai/codex) (OpenAI) |
| `mistral-vibe` | [Mistral Vibe CLI](https://github.com/mistralai/mistral-vibe) (opt-in/experimental) |
| `opencode` | [OpenCode](https://opencode.ai/) |
Expand All @@ -21,6 +21,8 @@ Built-in tool profiles:

Tool profiles live in `extensions/tools/<tool>/spec.yaml` (`kind: sandbox`) and declare the command, config directory, optional skills directory, QEMU microVM settings, and auth providers (API key vars, auth files, OAuth ports).

Which one runs is asked once on the first interactive session and then read from the `tool` key; the IDE profiles are selected explicitly with `--tool theia`. See [Tool selection](cli-reference.md#tool-selection).

## Base Images

The default base image is `debian:trixie-slim`. Override with `--base-image`:
Expand Down Expand Up @@ -54,7 +56,7 @@ Unsupported fields (`dockerComposeFile`, `features`, `remoteEnv`, `initializeCom
## Image Variants

Images are per-tool: each agent gets its own image, selected with `--tool`
(default: `claude`).
or the `tool` key.

| Flag | Image tag | Description |
|------|-----------|-------------|
Expand Down
37 changes: 37 additions & 0 deletions internal/app/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,40 @@ var backendFreeActions = map[string]bool{
func actionUsesBackend(action string) bool {
return !backendFreeActions[action]
}

// toolFreeActions never read the default tool: they list or manage sessions,
// extensions, configuration, and host-side setup, and read --tool at most as
// an explicit filter. The unset "auto" tool stays unresolved for them, so they
// neither ask which agent to use nor fail for lack of an answer.
var toolFreeActions = map[string]bool{
"attach": true,
"config": true,
"extension-list": true,
"features": true,
"img-import": true,
"ps": true,
"review-target": true,
"ssh-init": true,
"status": true,
"theia": true,
"theia-next": true,
"tools": true,
"validate-extensions": true,
cli.ActionExtensionManage: true,
}

// actionNeedsTool reports whether an invocation commits to a concrete tool: it
// starts a session, builds an image, or scopes policy, stores, or containers by
// tool. `update` with explicit targets, `stop <session>` and `cleanup --all`
// name their scope themselves and never read the default.
func actionNeedsTool(parsed cli.Result) bool {
switch parsed.Action {
case "update":
return len(parsed.Options.UpdateTools) == 0
case "stop":
return len(parsed.Options.CmdArgs) == 0
case "cleanup":
return !parsed.Options.CleanupAll
}
return !toolFreeActions[parsed.Action]
}
16 changes: 15 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,22 @@ func Run(args []string) int {
cliOpts := parsed.Options
cliSources := parsed.Sources
opts, toolDefaults, hasToolDefaults := config.ResolveOptionsForTool(cliOpts, cliSources, globalDefaults, projectDefaults, "")
// The tool is resolved before anything reads it: per-tool overrides,
// image identity and store paths all key on the concrete name, so the
// options are layered again once the unset "auto" has an answer. Verbs
// that never read the default tool leave it unresolved.
if actionNeedsTool(parsed) {
tool, err := resolveTool(opts.Tool, promptAllowed(parsed))
if err != nil {
logx.Errorf("%v", err)
return 1
}
if tool != opts.Tool {
opts, toolDefaults, hasToolDefaults = config.ResolveOptionsForTool(cliOpts, cliSources, globalDefaults, projectDefaults, tool)
}
}
if actionUsesBackend(parsed.Action) {
resolveBackend(&opts, backendPromptAllowed(parsed))
resolveBackend(&opts, promptAllowed(parsed))
}
sources := opts.Sources
parsed.Options = opts
Expand Down
Loading
Loading