# Architecture (https://jackin.tailrocks.com/reference/getting-oriented/architecture/)



<Aside type="note">
  This page is **for contributors** — anyone who wants to understand or
  work on jackin' itself. Operators do not need to read it to use
  jackin'; the [Operator Guide](/guides/workspaces/) and
  [Concepts](/getting-started/concepts/) cover everything an operator
  needs.
</Aside>

## High-level architecture [#high-level-architecture]

<ArchitectureDiagram />

jackin' deliberately uses your existing Docker engine as the operator control plane. For host-side Docker API calls, jackin' honors `DOCKER_HOST` when the operator sets it to a non-empty value (matching Docker CLI's own treatment of an empty `DOCKER_HOST=` as unset), including TLS endpoints — when `DOCKER_HOST` points at a `tcp://` daemon and `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` are set, jackin' performs the mutual-TLS handshake with those client certificates, so a remote daemon or a Docker-in-Docker socket on `:2376` works out of the box. Otherwise it inspects the active Docker CLI context — including any per-shell `DOCKER_CONTEXT` override — and connects to that context's Docker host when the context is a local socket or plain HTTP/TCP endpoint, so API calls such as network creation land on the same daemon that later `docker run` commands use. SSH Docker contexts are rejected for now because the Bollard dependency build cannot reproduce the Docker CLI's SSH transport, and TLS *contexts* are rejected because jackin' does not yet wire a context's TLS material into the connection (a directly-set `DOCKER_HOST` with explicit `DOCKER_CERT_PATH` is the supported path for TLS daemons).

That creates an important split:

* **build time** happens on the host Docker engine
* **runtime orchestration** creates the role container, DinD sidecar, certs volume, and per-instance network on that same selected host Docker engine
* **runtime Docker commands issued by the agent** go to the per-instance DinD sidecar

So jackin' gets host-side cache reuse for building agent images, while still keeping the agent away from your host Docker daemon during runtime.

## Design constraints [#design-constraints]

jackin' architecture is shaped by a few explicit decisions:

1. **Local development first.** The first target is a developer machine that already knows how to run Docker.
2. **Environment as source code.** Roles should be normal git repos reviewed like normal software.
3. **Workspace boundary separate from tool profile.** File visibility and installed tooling are different control planes on purpose.
4. **Operator-owned infrastructure, not black-box runtime.** The implementation should stay inspectable and patchable.
5. **Container isolation is acceptable for the first target.** jackin' does not claim a microVM boundary today.

## Async architecture [#async-architecture]

The host runtime is intentionally a Tokio `current_thread` runtime. The operator CLI is an orchestrator, not a high-throughput server: keeping one async scheduler thread makes ordering easier to reason about, keeps render and progress state deterministic, and forces expensive work to declare itself instead of disappearing into incidental blocking calls.

The rule is simple: render and progress tasks never block on synchronous I/O or CPU-heavy work. If a call can be async, it runs through the async abstraction; if it cannot be made async, the blocking boundary is explicit.

| Call shape                                               | Mechanism                             | Reason                                                                                                                                                                                                                           |
| -------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Async subprocess / command capture                       | `ShellRunner`                         | Centralizes subprocess execution, diagnostics, cancellation shape, and test fakes instead of sprinkling `Command` calls through launch code.                                                                                     |
| Docker lifecycle API calls                               | `DockerApi` / `BollardDockerClient`   | Keeps Docker inspect/list/create/remove operations async and mockable; interactive `docker build`, `docker run -it`, and `docker exec -it` stay on the Docker CLI because their terminal behavior is the compatibility contract. |
| Unavoidable synchronous filesystem, archive, or CPU work | `tokio::task::spawn_blocking`         | Moves work that can stall the scheduler off the render/progress thread while keeping the boundary visible in review.                                                                                                             |
| Independent launch stages                                | `tokio::try_join!` or sibling futures | Runs unrelated setup concurrently while preserving fail-fast error propagation and one diagnostics timeline.                                                                                                                     |
| TUI rendering / progress updates                         | Non-blocking state updates only       | The render task reads state and paints; it does not perform filesystem, network, Docker, or subprocess work.                                                                                                                     |

This is a cross-cutting TUI rule too: background I/O reports into typed state, and the render path consumes that state. A review that finds `std::thread::sleep`, blocking `std::fs`, or direct synchronous command capture in a render-thread crate should treat it as an architecture bug unless the call is already behind an explicit blocking boundary.

## Image layers [#image-layers]

<ImageLayers />

The role repo owns the environment layer. jackin' owns the derived agent layer.

That means a role defines the tools and conventions, while jackin' injects the agent-specific pieces such as Claude, Codex, Amp, Kimi, or OpenCode installation, the entrypoint, and UID/GID remapping.

## Role repository tooling [#role-repository-tooling]

Role repositories are normal source repositories, so jackin' keeps role maintenance split between two surfaces. The main operator CLI owns desktop ergonomics: its role-authoring dispatch validates checked-out role repositories, applies manifest migrations to local copies, and writes small starter repositories for authors. That path lives in <RepoFile path="crates/jackin/src/role_authoring.rs">crates/jackin/src/role\_authoring.rs</RepoFile> and reuses the same manifest migration registry and repository contract checks used by runtime launch.

Automation stays on the small role-focused binary in <RepoFile path="crates/jackin/src/bin/role.rs">crates/jackin/src/bin/role.rs</RepoFile>. CI workflows and future Renovate-style migration jobs should not depend on the full operator CLI just to validate a role repo or restamp a manifest; they call the standalone validator, which shares <RepoFile path="crates/jackin-manifest/src/repo.rs">crates/jackin-manifest/src/repo.rs</RepoFile> and <RepoFile path="crates/jackin-manifest/src/migrations.rs">crates/jackin-manifest/src/migrations.rs</RepoFile> with the desktop path.

## Container lifecycle [#container-lifecycle]

### Loading an agent [#loading-an-agent]

Every `jackin ...` invocation mints a run diagnostics ID before command-specific work starts. Launch paths write compact breadcrumbs to `~/.jackin/data/diagnostics/runs/<run_id>.jsonl`; `--debug` adds detailed command, Docker, resolver, auth, cache, materialization, cleanup, and build traces to that artifact. The operator-facing launch surface shows only product-level progress and the run handle.

1. **Resolve role** — map the selector to a repo, clone or update it, and reject dirty cached checkouts
2. **Validate the repo contract** — require `jackin.role.toml` and a valid Dockerfile path, validate manifest strictly
3. **Resolve environment variables** — prompt the user for interactive env vars declared in the manifest
4. **Generate a derived build context** — copy the repo, inject agent assets (entrypoint + runtime hooks), and render a derived Dockerfile
5. **Build the image on the host Docker engine** — reusing host-side Docker cache where possible
6. **Create a per-agent Docker network**
7. **Start a privileged `docker:dind` sidecar** with TLS enabled and a shared certificate volume
8. **Write Capsule launch config** — serialize the role key, workspace workdir, ordered supported-agent list, and manifest model overrides to `~/.jackin/sockets/<container>/agent.toml`, which is bind-mounted at `/jackin/run/agent.toml`.
9. **Start the agent container detached** (`docker run -d ... <image> <agent>`) with `jackin-capsule` as PID 1. The trailing `<agent>` argv selects only the initial tab; it is not a container-level environment variable.
10. **Attach the operator terminal** via `docker exec -it <container> /jackin/runtime/jackin-capsule`. The client connects to the PID 1 daemon over `/jackin/run/jackin.sock` and renders the in-container multiplexer.

Launch progress is emitted through <RepoFile path="crates/jackin-runtime/src/runtime/progress.rs">crates/jackin-runtime/src/runtime/progress.rs</RepoFile>. Rich terminals render the Ratatui launch cockpit; low-capability or non-interactive terminals receive compact deterministic stage lines. The public stages are stable (`identity`, `role`, `credentials`, `construct`, `agent binaries`, `derived image`, `workspace`, `network`, `sidecar`, `capsule`, `hardline`) even when implementation details change underneath. The rich cockpit may briefly dwell on stage changes so very fast work still produces a legible full-sequence rail before the terminal is handed to the Capsule client.

| Stage label      | Technical work behind the label                                                                                                                                                                                                                                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identity`       | Resolve the launch target, workspace/path label, role selector, requested agent, instance name, image tag, run diagnostics handle, and operator-visible loading title.                                                                                                                                                                        |
| `role`           | Resolve the role source from config or command flags, acquire/update the role-repo cache, check out the requested ref/branch, and load the role manifest.                                                                                                                                                                                     |
| `credentials`    | Inspect host-side agent/GitHub/1Password credential availability read-only, materialize per-instance auth state under jackin-owned state, and prepare the credential mounts/env that the container receives.                                                                                                                                  |
| `construct`      | Verify or pull the shared construct base image (`projectjackin/construct:trixie`) that the derived role image builds from.                                                                                                                                                                                                                    |
| `agent binaries` | Resolve and cache the declared agent runtime binaries plus the matching `jackin-capsule` binary under jackin-owned host cache state. If release metadata lookup fails after the metadata TTL expires, jackin' falls back to the newest executable cached agent binary and records a diagnostics warning; a cold cache still fails the launch. |
| `derived image`  | Generate the role-specific `DerivedDockerfile`, assemble the temporary Docker build context, copy cached runtime assets into it, run `docker build`, and capture Docker stdout/stderr to a plain run sidecar log on failure.                                                                                                                  |
| `workspace`      | Prepare the mounted workspace/directory, optional workspace git-pull-on-entry work, per-instance manifest/index records, runtime directories, mount list, and Capsule launch config.                                                                                                                                                          |
| `network`        | Create or reconcile the per-instance Docker network and related host-side runtime names.                                                                                                                                                                                                                                                      |
| `sidecar`        | Start or recover the DinD sidecar container and its certs volume for the role container.                                                                                                                                                                                                                                                      |
| `capsule`        | Start the detached role container with `jackin-capsule` as PID 1, the selected agent argv, resolved mounts, runtime config, labels, and environment.                                                                                                                                                                                          |
| `hardline`       | Attach the foreground terminal to the running Capsule client with `docker exec -it`, then classify the post-session container state for cleanup, preservation, or recovery.                                                                                                                                                                   |

The cinematic launch animations are gated by jackin-managed role-container discovery. A hyperspace-warp intro (preceded by typed cyberpunk-style phrases) plays before entering the first container, and a decelerating warp outro plays on foreground exit — followed by a duration caption when no role containers remain, or a still-running summary when others do. Digital rain itself lives only in the loading cockpit, not at these boundaries. Discovery failure skips the animation and records the reason in diagnostics instead of blocking launch.

The detached-start + exec split decouples the container's lifetime from the foreground attach: closing the operator's terminal detaches from the client, while the PID 1 daemon and live PTYs keep running. `jackin hardline` reconnects to the daemon or asks it to spawn a new session. Cleanup distinguishes three post-session states:

* **running** → terminal was closed; preserve the container for `jackin hardline` to reconnect
* **stopped with exit 0** → the user exited cleanly; tear down container, DinD, network, and certs volume
* **stopped with non-zero exit or OOM** → crash; preserve the container and DinD so `jackin hardline` can restart in place

Every fresh launch gets a unique DNS-safe instance identity. Existing running or stopped instances are not fresh-launch blockers; they remain discoverable through `hardline` and the console. Missing recoverable state is the case that can prompt or block before a fresh load. The detailed contributor contract lives in [Runtime Instance Model](/reference/runtime/runtime-instance-model/).

### Reconnecting [#reconnecting]

`jackin hardline` inspects the target container. If it's running, it attaches with `docker exec ... /jackin/runtime/jackin-capsule`; `hardline --new --agent <agent>` uses `jackin-capsule new <agent>` so agent selection travels as argv. If it's stopped with a non-zero exit (crash / OOM), it restores the required DinD sidecar/network/certs when needed, restarts the role container, and attaches. If the role container itself is missing but indexed recoverable state exists, hardline can rebuild the runtime around jackin-managed local state. Clean exits (exit code 0) are treated as completed sessions — use `jackin load` to start a fresh instance.

### Capsule supervisor and session model [#capsule-supervisor-and-session-model]

Every role container runs the jackin' Capsule (<RepoFile path="crates/jackin-capsule/src/main.rs">crates/jackin-capsule/src/main.rs</RepoFile>) as PID 1. In daemon mode it reaps children, manages PTYs, renders the multiplexer, and serves the socket API. `SIGTERM` / `SIGINT` trigger orderly shutdown, and the daemon exits when the last live session ends so Docker reports a clean container exit. The full Capsule design is documented in [jackin' Capsule](/reference/capsule//).

Sessions are daemon-owned PTYs inside the running container. All agent sessions are equal — there is no "primary" or "secondary" distinction.

| Session type  | Identity                             | Created by                                           | Reconnectable                                                           |
| ------------- | ------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------------------- |
| Agent session | daemon session id plus agent slug    | `jackin load`, `jackin hardline`, or console `a`/`A` | Yes — `jackin hardline` attaches to the daemon or creates a new session |
| Shell         | daemon session id with no agent slug | `hardline --shell` or console `x`/`X`                | No — intended as an ephemeral zsh pane                                  |

When an agent session exits cleanly (the operator types `/exit` or the agent finishes), the daemon removes that pane. If no live sessions remain, PID 1 exits and the host cleanup path tears down the role container, DinD sidecar, certs volume, and network.

`JACKIN_AGENT` is set per agent PTY by <RepoFile path="crates/jackin-capsule/src/session.rs">crates/jackin-capsule/src/session.rs</RepoFile>, immediately before `/jackin/runtime/entrypoint.sh` runs. The daemon's own process does not carry `JACKIN_AGENT`, so shell panes and future mixed-agent sessions do not inherit a misleading container-wide agent value.

Role identity, workspace workdir, supported-agent order, and model overrides are not duplicated into container-wide environment variables. The daemon reads those values from `/jackin/run/agent.toml`, keeping all jackin-owned in-container runtime state under `/jackin/`.

Session state is not yet reconciled against the live container on console refresh — Phase 4 of the [Console agent session control](/reference/roadmap/console-agent-session-control/) roadmap covers this work. The `sessions` field in the instance manifest is written but not populated until reconciliation lands.

Implementation: <RepoFile path="crates/jackin-capsule/src/daemon.rs">crates/jackin-capsule/src/daemon.rs</RepoFile>, <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs">crates/jackin-runtime/src/runtime/launch.rs</RepoFile> (`launch_role_runtime`), <RepoFile path="crates/jackin-runtime/src/runtime/attach.rs">crates/jackin-runtime/src/runtime/attach.rs</RepoFile> (`reconnect_or_create_session`, `spawn_agent_session`, `spawn_shell_session`).

### Stopping [#stopping]

`jackin eject` removes:

1. the agent container
2. the DinD sidecar
3. the TLS certs volume (`{container}-dind-certs`)
4. the per-agent network

Persisted state remains unless you also purge it.

## Networking [#networking]

Each agent gets an isolated Docker network:

* network name is derived from the agent container name
* the agent container and DinD sidecar share that network
* the agent reaches the daemon through `DOCKER_HOST=tcp://{dind}:2376` with TLS mutual authentication
* the agent reaches published services launched via DinD through `JACKIN_DIND_HOSTNAME={dind}`
* different agents do not share a network by default

The DinD sidecar auto-generates TLS certificates at startup into a shared Docker volume. The agent container mounts the client certificates read-only and sets `DOCKER_TLS_VERIFY=1` so all Docker CLI commands are authenticated.

## Terminal capability model [#terminal-capability-model]

Pane PTYs run with a stable `TERM=xterm-256color` baseline and `COLORTERM=truecolor`. jackin' does not copy the host terminal's `$TERM` into the long-lived container environment, because a preserved instance can be reattached later from a different terminal app. A pane that started while the operator used Ghostty must still be safe to reattach from Kitty, iTerm, Warp, SSH, or a plain xterm-compatible terminal. `COLORTERM=truecolor` is a deterministic capability hint for agent CLIs and TUIs, not a host-specific terminal identity.

Terminal-specific enhancements are attached-client state instead. Every foreground attach (`jackin load`, `jackin hardline`, and console attach) runs a short-lived Capsule client through `docker exec`; that client reports its current `TERM`, `TERM_PROGRAM`, and `COLORTERM` in the attach handshake. The PID 1 daemon stores that identity only for the active client and refreshes it on every takeover, so jackin-owned output enhancements follow the terminal the operator is using now.

Ghostty, Kitty, and iTerm get native enhancements through that active-client capability path. Features that are raw terminal protocols, such as extended-key sequences, OSC 52 clipboard writes, OSC 8 hyperlinks, OSC 9 notifications, and focused-pane title updates, continue to travel over the attach stream when allowed. Enhancements that require known outer-terminal support, such as OSC 22 pointer-shape feedback, are enabled only when the active client advertises a compatible identity.

## Capsule terminal capability model [#capsule-terminal-capability-model]

The container no longer runs tmux for agent sessions. The Capsule owns the PTYs directly, so terminal behavior that used to come from tmux options is implemented at the Capsule/client boundary:

| Former tmux concern            | Capsule behavior                                                                                                                                                                                           |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Modified keys                  | Client-side input parsing preserves xterm/Kitty/CSI-u key sequences and forwards pane input unmodified unless it is a jackin'-owned palette or prefix command.                                             |
| Terminal feature advertisement | Pane PTYs run with `TERM=xterm-256color` and `COLORTERM=truecolor`; outer-terminal identity is reported per attach and used only for client-side enhancements.                                             |
| Focus events                   | The attach client enables focus reporting on the outer terminal and forwards focus-in/focus-out only to panes that requested it.                                                                           |
| OSC passthrough                | Focused-pane OSC 0/1/2 titles, OSC 8 hyperlinks, OSC 9 notifications, and OSC 52 clipboard writes are forwarded with per-family opt-outs. OSC 7 is captured for pane titles and not forwarded to the host. |
| Escape timing                  | The input parser treats Escape as data for the pane except when a jackin' dialog is open; dialogs receive normalized legacy keys even after an embedded TUI enables progressive keyboard reporting.        |
| Mouse                          | The client keeps the outer terminal in SGR any-event mode. The daemon handles clicks on jackin' chrome and forwards pane mouse events only when the pane's program opted into mouse reporting.             |

Implementation: <RepoFile path="crates/jackin-capsule/src/client.rs">crates/jackin-capsule/src/client.rs</RepoFile>, <RepoFile path="crates/jackin-capsule/src/tui/input.rs">crates/jackin-capsule/src/tui/input.rs</RepoFile>, <RepoFile path="crates/jackin-capsule/src/session.rs">crates/jackin-capsule/src/session.rs</RepoFile>, and <RepoFile path="crates/jackin-capsule/src/daemon.rs">crates/jackin-capsule/src/daemon.rs</RepoFile>.

Operators running `jackin console` from inside their own tmux session need matching host-side configuration so that modified keys are forwarded through the host tmux before they reach the container. See the [Running inside tmux](/guides/tmux/) operator guide.

## Workspace materialization [#workspace-materialization]

Workspace mounts pass through three distinct shapes between operator intent and a Docker bind-mount. Keeping these separate is what lets per-mount isolation slot in cleanly:

| Shape                   | Origin                         | Owns                                                                                                                                                                                   |
| ----------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WorkspaceConfig`       | `~/.config/jackin/config.toml` | Persisted operator intent — raw `src`/`dst`/`readonly`/`isolation` fields per mount.                                                                                                   |
| `ResolvedWorkspace`     | `resolve_load_workspace`       | Expanded host paths plus the merged composition of workspace, ad-hoc, and global mounts. Still independent of the actual container instance.                                           |
| `MaterializedWorkspace` | post-name-claim materializer   | Final host paths Docker should bind-mount for *this* container. Isolated checkouts have been created on disk; isolated `src` values point at the per-container worktree or clone path. |

Materialization happens **after** jackin claims the final unique container name and the per-container state directory exists. For each non-`shared` mount, materialization is idempotent: worktree mode creates a scratch branch from host `HEAD`, clone mode runs a local clone of the host repo's current branch, and both modes record `base_commit`. Later loads to the same container reuse the existing checkout as-is — no reset, no rebase, no fast-forward.

On first worktree creation for a host repo, jackin' enables `extensions.worktreeConfig` (bumping `core.repositoryformatversion` to 1 if needed) so per-worktree config writes via `git config --worktree` don't leak into the shared host `.git/config`.

### `isolation.json` [#isolationjson]

Materialized isolated mounts are recorded at:

```
<data_dir>/<container>/.jackin/isolation.json
```

The file lives next to the per-container agent state and `.jackin/` metadata and is the source of truth for `purge` after the operator has changed (or removed) the workspace config. Each record contains at least:

| Field            | Purpose                                                                              |
| ---------------- | ------------------------------------------------------------------------------------ |
| `workspace`      | Workspace name at materialization time                                               |
| `mount_dst`      | Container destination path (the mount identity)                                      |
| `original_src`   | Resolved host `src` recorded for source-drift detection                              |
| `isolation`      | Mode (`worktree` or `clone`)                                                         |
| `worktree_path`  | Materialized worktree or clone path on the host                                      |
| `scratch_branch` | Worktree scratch branch name (`jackin/scratch/<container>`); empty for clone records |
| `base_commit`    | Host `HEAD` at first materialization                                                 |
| `selector_key`   | Agent selector key                                                                   |
| `container_name` | Final container name                                                                 |
| `status`         | `active` \| `preserved_dirty` \| `preserved_unpushed`                                |

Successful cleanup removes the corresponding record rather than keeping a historical `cleaned` entry.

### Foreground session finalization [#foreground-session-finalization]

Every command that opens or reopens an agent foreground session — `jackin load`, `jackin hardline`, the operator console — funnels through the same post-session finalizer. After the foreground `docker exec ... jackin-capsule` attach returns, jackin' inspects the container and decides:

* **Container is still running** → preserve everything for `jackin hardline` to reconnect later. No isolated-checkout action.
* **Stopped, non-zero exit or OOM** → preserve the container, DinD, and isolated checkouts so `jackin hardline` can restart in place.
* **Stopped, exit code 0 (clean)** → attempt safe isolated-checkout cleanup. A checkout is safe to delete only when it has no uncommitted changes and **every** local branch in the checkout is safe — agents routinely rename the worktree scratch branch to `feature/*` before opening a PR, and clone mode starts on the host branch directly, so the safety check enumerates `git for-each-ref refs/heads/` rather than inspecting only the recorded scratch branch. A branch is safe when its tip is at `base_commit`, when its upstream is configured and `git rev-list <upstream>..<branch>` is empty, or when its upstream tracking is `[gone]` (the squash-merged-and-pruned heuristic — without it, the GitHub-default merge-and-delete-branch flow leaves a graveyard of preserved checkouts).
* **Clean exit with unsafe cleanup**, interactive rich terminal → ask the operator in a rich dialog whether to return to the agent (restart + re-attach), preserve recovery state, or force-delete. The prompt wording branches on whether the checkout is dirty (`uncommitted changes`) or clean-but-unpushed (`unpushed commits on a local branch`). If the dialog cannot render, show an error dialog in the same rich surface and preserve the worktree.
* **Clean exit with unsafe cleanup**, non-interactive → preserve the recovery state and print the checkout path plus the per-reason phrasing and how to return, inspect, or discard.

The same finalizer runs after `hardline` attaches, so the cleanup decision is independent of how the foreground session was opened.

## State management [#state-management]

```
~/.jackin/
├── roles/                         # Cached role repos
│   ├── agent-smith/               # Flat role (no namespace)
│   └── chainargos/                # Namespace directory
│       └── agent-brown/           # Namespaced role
├── cache/                         # Shared caches (version checks and other rebuildable data)
├── data/
│   ├── instances.json             # Rebuildable index of all known instances
│   └── <container-base>/          # Persisted state per instance
│       ├── claude/                # Claude auth handoff (provisioned when Claude is a supported agent)
│       │   ├── credentials.json   # Forwarded OAuth credentials under auth_forward=sync
│       │   └── account.json       # Forwarded Claude Code account metadata under sync/token modes
│       ├── codex/                 # Codex auth handoff (provisioned when Codex is a supported agent)
│       │   └── auth.json          # ChatGPT OAuth tokens (synced from host ~/.codex/auth.json under auth_forward=sync)
│       ├── amp/                   # Amp auth handoff (provisioned when Amp is a supported agent)
│       │   └── secrets.json       # Amp API key or OAuth token (synced from host ~/.local/share/amp/secrets.json under auth_forward=sync)
│       ├── kimi-code/             # Kimi auth handoff (provisioned when Kimi is a supported agent)
│       │   ├── config.toml        # Kimi config (synced from host ~/.kimi-code/config.toml under auth_forward=sync)
│       │   ├── credentials/       # Kimi credentials (synced from host ~/.kimi-code/credentials under auth_forward=sync)
│       │   └── device_id          # Kimi device id (synced from host ~/.kimi-code/device_id under auth_forward=sync)
│       ├── opencode/              # OpenCode auth handoff (provisioned when OpenCode is a supported agent)
│       │   └── auth.json          # OpenCode provider credentials (synced from host ~/.local/share/opencode/auth.json under auth_forward=sync)
│       ├── home/                  # Durable agent home state mounted into the container
│       │   ├── .claude/           # Claude Code conversation history, runtime-local plugins, settings
│       │   ├── .claude.json       # Claude Code account metadata inside the durable home
│       │   ├── .codex/            # Codex conversation history, runtime-local config, login state
│       │   ├── .local/share/amp/  # Amp conversation history, runtime-local config, login state
│       │   ├── .kimi-code/        # Kimi runtime-local config and login state
│       │   └── .local/share/opencode/  # OpenCode provider credentials, model preferences
│       ├── state/                 # jackin runtime markers mounted at /jackin/state
│       ├── .config/gh/            # GitHub CLI authentication (agent-neutral)
│       └── .jackin/               # jackin runtime metadata (agent-neutral)
│           ├── instance.json      # Per-instance manifest (identity, role, agent, mounts, status)
│           └── isolation.json     # Per-mount isolation records (when applicable)
```

This is what persists across sessions:

* Claude/Codex/Amp/Kimi/OpenCode history, runtime-local settings, plugins, and authentication state stored under the per-instance durable home
* jackin' plugin metadata
* `gh` state acquired inside the agent container

This is what does **not** persist:

* packages installed interactively into the agent container
* ad-hoc root filesystem changes outside mounted paths
* DinD images, containers, and cache

That means jackin' restores only host-backed state: workspace mounts, isolated worktrees/clones, and the per-instance agent home under `~/.jackin/data/<container>/home/`. Anything installed or edited only in the container writable layer is intentionally disposable and will not be restored after Docker deletes the container.

jackin'' is biased toward **reproducible roles** rather than **mutable long-lived sandboxes**.

If a tool should always exist, put it in the role repo Dockerfile instead of installing it ad hoc during a session.

## Why Docker instead of microVMs? [#why-docker-instead-of-microvms]

Because jackin' is solving a slightly different problem.

Using plain Docker lets jackin':

* reuse a toolchain developers already have
* keep the implementation smaller and easier to inspect
* build agent images with normal Docker workflows
* share host-side build cache across roles
* stay local-first without introducing a separate VM product layer

The cost of that choice is straightforward:

* weaker isolation than a microVM
* shared host kernel
* privileged DinD sidecars
* no hypervisor boundary

If the main thing you need is the strongest local boundary for autonomous agents on a laptop, microVM-based products such as Docker Sandboxes are ahead.

## Current technical debt and limitations [#current-technical-debt-and-limitations]

* **DinD transport uses auto-generated TLS certificates.** The certificates are generated per-session and not pinned to a CA chain you control.
* **Claude installation is not fully pinned.** The derived image installs Claude Code from the upstream installer.
* **Derived build context rejects symlinks.** Role repos with symlink-heavy layouts are not supported yet.
* **Cached role repos must stay clean.** jackin' refuses to build from a dirty or origin-mismatched cache.
* **Container mutability is intentionally thin.** That keeps sessions cleaner, but makes ad-hoc package installs non-persistent.
* **Isolation-mode edits are guarded only when preserved state already records the previous mode.** Source drift is checked before workspace edits are saved; changing only `mounts[].isolation` can still take effect on the next launch without a pre-save warning when there is no conflicting preserved record.

## Technology stack [#technology-stack]

jackin' is built in Rust. The runtime dependencies, grouped by what they own, match <RepoFile path="Cargo.toml">Cargo.toml</RepoFile>:

| Concern                       | Crates                                                                     |
| ----------------------------- | -------------------------------------------------------------------------- |
| CLI parsing & manpages        | `clap`, `clap_mangen`                                                      |
| Terminal UI                   | `ratatui`, `crossterm`, `ratatui-textarea`, `tui-widget-list`, `dialoguer` |
| Table output                  | `tabled`                                                                   |
| Configuration (parse + edit)  | `serde`, `serde_json`, `toml`, `toml_edit`                                 |
| XDG paths and process locking | `directories`, `fs2`                                                       |
| Dockerfile parsing            | `dockerfile-parser-rs`                                                     |
| Error handling                | `anyhow`, `thiserror`                                                      |
| Colored output                | `owo-colors`                                                               |
| Filesystem helpers            | `tempfile`                                                                 |
| Open external URLs            | `open`                                                                     |
| Test helpers (dev)            | `assert_cmd`, `predicates`                                                 |

The project targets **Rust edition 2024** with a current MSRV of **1.94** (see `rust-version` in <RepoFile path="Cargo.toml">Cargo.toml</RepoFile>) and `unsafe_code = "forbid"` at the crate root. Clippy lints are configured with `correctness` and `suspicious` set to `deny` and `complexity`/`style`/`perf`/`pedantic`/`nursery` set to `warn` — see <RepoFile path="Cargo.toml">Cargo.toml</RepoFile> for the full lint table.

When this list drifts from <RepoFile path="Cargo.toml">Cargo.toml</RepoFile>, <RepoFile path="Cargo.toml">Cargo.toml</RepoFile> wins. Update both in the same PR.

## Async architecture and the "no blocking on the render thread" rule [#async-architecture-and-the-no-blocking-on-the-render-thread-rule]

Both the host CLI (`crates/jackin/src/main.rs`) and the capsule daemon (`crates/jackin-capsule/src/main.rs&#x60;) run on a **`current_thread` Tokio runtime**. The render/event loop and all spawned tasks share a single OS thread. This is intentional:

* The host CLI holds `!Send` terminal raw-mode state and signal handlers that must stay on the OS main thread.
* The capsule daemon's multiplexer loop, PTY readers, and socket accept loop communicate via `mpsc` channels — the single-thread model prevents data races without locking.

**Rule: no blocking syscall on the tokio runtime thread.** Any call that can block for more than a few microseconds must be offloaded. Two mechanisms:

| Call type                                                         | Mechanism                                         |
| ----------------------------------------------------------------- | ------------------------------------------------- |
| Subprocess execution (git, gh, op) that is a one-shot output read | `tokio::task::spawn_blocking`                     |
| Long-running subprocess with output streaming                     | `tokio::process::Command` + `lines()`             |
| CPU-bound work (hashing, compression)                             | `tokio::task::spawn_blocking`                     |
| File I/O in a hot async path                                      | `tokio::fs` or `spawn_blocking`                   |
| Sleep / poll wait                                                 | `tokio::time::sleep` — never `std::thread::sleep` |

`spawn_blocking` works on the `current_thread` runtime — it uses Tokio's separate blocking thread pool, so the runtime thread keeps polling the render task while the blocking work runs elsewhere.

**Known open sites (Defect 43).** A small number of blocking calls remain in the provisioning path. The highest-priority one is `RoleState::prepare` in <RepoFile path="crates/jackin-runtime/src/instance.rs">crates/jackin-runtime/src/instance.rs</RepoFile>, which runs `gh` CLI, macOS keychain (`security`), and filesystem copies directly on the async provisioning task — and is therefore the most likely rain-freeze cause when git auth or 1Password are involved. Wrap it in `spawn_blocking` and clone the necessary inputs for the closure. Subsequent conversions: `op_cli.rs` poll loops, and any remaining `std::process::Command` in async-reachable paths.

**Parallelism: `try_join!` for independent stages.** The launch pipeline in <RepoFile path="crates/jackin-runtime/src/runtime/launch/launch_pipeline.rs">crates/jackin-runtime/src/runtime/launch/launch\_pipeline.rs</RepoFile> already uses `tokio::join!` for some independent steps. The pattern is `try_join!(stage_a, stage_b)` for stages with no data dependency, so they run concurrently while the render task keeps ticking. Each stage emits its own `stage_started` / `stage_done` events to the progress channel; the shared-state mutex in `SharedView` handles out-of-order updates already. Extending this to more stage groups is incremental and safe.

## AgentRuntime and Provider registry [#agentruntime-and-provider-registry]

Adding a new agent runtime or provider must be one registration, not N call-site edits. This is enforced through two sealed adapter registries.

**`AgentRuntime` trait** (`crates/jackin-core/src/agent/runtime.rs`). Every agent variant (`claude`, `codex`, `amp`, `kimi`, `opencode`) is represented by a zero-sized adapter struct that implements `AgentRuntime`. `Agent::runtime()` returns `&'static dyn AgentRuntime` — a compile-time-known dispatch point. Adding a sixth built-in runtime means adding the enum variant, adapter file, registry entry, and the small number of centralized named-field accessor rows that remain because the public TOML schemas still expose named agent sections.

Key trait methods:

* `slug() / label()` — wire identifier and display name.
* `install_block(source)` — generates the Dockerfile `COPY`/`RUN` block for this agent.
* `required_env_var()` — the env var whose presence signals that the agent is installed.
* `supported_modes()` — which `AuthForwardMode` values are valid for this agent.
* `state_paths()` — `AgentStatePaths { credential_dir, credential_file, folder_env_var }` — the per-agent credential location on the host and the environment hint for the operator. Used by provisioning and by `sync_source_dir` resolution.
* `parse_version(output)` — parses `--version` stdout to extract a semver string, replacing five per-agent branches.

**`ProviderAdapter` trait** (`crates/jackin-protocol/src/provider_adapter.rs`). Same sealed pattern for providers (Anthropic, Zai, MiniMax, Kimi). `Provider::adapter()` returns `&'static dyn ProviderAdapter`. `Provider::available_for(agent_slug, has_key: impl Fn(Provider) -> bool)` replaces the four-positional-bool form.

Key trait methods:

* `label()` — display name.
* `needs_key_for_agent(slug)` — whether an explicit API key is required (Anthropic + claude: no; everything else: yes).
* `supports_agent(slug)` — whether this provider works with the given agent.
* `env_overrides(token)` — the env vars to inject at spawn to redirect the agent to this provider.
* `key_env_var()` — the env var name that holds the provider's API key (`ZAI_API_KEY`, `MINIMAX_API_KEY`, `KIMI_CODE_API_KEY`, or `None` for Anthropic).
* `opencode_model()` — the `provider/model` string for OpenCode's `-m` flag.

**Sealed trait pattern.** Both traits are sealed via a `pub(crate) mod private { pub trait Sealed {} }` guard so external crates cannot add providers or runtimes. The built-in set is the intended complete set; adding one means changing this crate, which triggers a review.
