Skip to content

Ephemeral Mount Modes (`tmpfs`, `ephemeral`)

Status: Open — design proposal (Phase 1, Agent Orchestrator Research Program)

jackin’s per-mount isolation vocabulary is shared | worktree | clone. These modes describe git semantics for the mounted path. They don’t cover a different axis the operator also cares about: storage kind.

Specifically, two storage shapes have no declarative way to be expressed today:

  • A path the agent can read and write, but whose contents should NOT appear on the host filesystem and SHOULD reset between launches. (Examples: ~/.gradle/daemon, an agent’s /var/tmp, an OpenCode session-state directory that should be per-instance.)
  • A path that lives in tmpfs (memory-backed) for performance or for guaranteed teardown when the container exits. (Examples: /tmp, a temporary ~/.ssh shadow during a rebase script.)

Today an operator can only achieve the first by writing a Docker volume into a --mount arg and managing its lifecycle by hand, and the second by passing --tmpfs ad-hoc. Neither lives in jackin.role.toml or the workspace config.

multicode confirms this is a real, frequently-used distinction. Their example config.toml declares 7 paths under isolated and 1 under tmpfs — both essential to keeping concurrent agents from corrupting each other’s caches and to keeping host state clean.

  • Parallel agents share host caches today. Two agents both running gradle build race on ~/.gradle/daemon because jackin’ has no way to declare “this directory is per-instance, not host-shared”.
  • Toolchain hygiene matters at scale. With the autonomous task queue in place, an agent fleet runs hundreds of builds a day. Each one should start with a clean per-instance scratch space; today, they don’t.
  • /tmp should default to tmpfs. Many roles bake configuration assuming /tmp is fast and ephemeral. jackin’s default /tmp is the container’s own filesystem — fine for correctness, but larger and slower than necessary.
  • This is the only mount-mode gap found in the verification pass comparing jackin’ against multicode’s [isolation] block. Closing it brings the mount surface to parity.

Sources:

  • README — Isolation (mounting policy and Apple-container subsections describe per-workspace isolation rules)
  • Config — config.toml [isolation] block — the isolated = [...] and tmpfs = [...] lists in the example
[isolation]
isolated = [
"~/.local/share/opencode",
"/var/tmp",
"~/.gradle/daemon",
"~/.gradle/.tmp",
"~/.gradle/kotlin-profile",
"~/.local/share/kotlin",
"~/.local/state/opencode",
]
tmpfs = [
"/tmp",
]

Semantics:

  • isolated: the path inside the workspace is mapped to a per-workspace dedicated mount (or tmpfs overlay). It’s writable while the workspace runs but not visible on the host and wiped per workspace start. Implementation: bwrap --tmpfs <path> overlays or a dedicated mount; the path doesn’t share with host.
  • tmpfs: explicit tmpfs mount. Memory-backed, ephemeral, gone on exit. Implementation: bwrap --tmpfs <path>.

The two are subtly different: isolated may be backed by disk for larger working sets (build artifacts), while tmpfs is always memory. multicode lets the operator pick.

Extend the per-mount isolation enum vocabulary by two more modes, plus extend the workspace mount schema so they don’t all need a host src.

The existing per-mount isolation schema requires a host src:

[[workspaces.X.mounts]]
src = "~/projects/x"
dst = "/workspace/x"
isolation = "worktree"

Add two modes that don’t need src:

[[workspaces.X.mounts]]
dst = "/tmp"
isolation = "tmpfs"
size = "512MiB" # optional; Docker default 64MiB is small
[[workspaces.X.mounts]]
dst = "/home/agent/.gradle/daemon"
isolation = "ephemeral"
# 'ephemeral' = per-instance Docker volume, persists across container
# restarts of the same instance, deleted on `purge`. Not bind-mounted
# to host.

V1 enum vocabulary becomes: shared | worktree | tmpfs | ephemeral (plus clone when V1.1 ships). Validation:

  • tmpfs and ephemeral reject src (no host source).
  • tmpfs and ephemeral reject readonly (writability is their whole point; if you want read-only, you want shared with readonly = true).
  • tmpfs accepts an optional size field with the same parsing rules as declarative resource limits.
  • Docker (today): tmpfs--tmpfs <dst>:size=<n>,mode=1777. ephemeral--mount type=volume,source=jackin-<container>-<slug>,target=<dst>, with the volume created on first load and deleted by purge (the per-instance cleanup helper already enumerates Docker volumes by label; add a label and reuse it).
  • Apple container (when selectable backends ships): use the equivalent allocation primitives.
  • bwrap (if it ever lands): --tmpfs for both modes; the ephemeral case is bwrap’s natural shape.

Roles can declare ephemeral defaults in jackin.role.toml:

[[runtime.mounts]]
dst = "/home/agent/.gradle/daemon"
isolation = "ephemeral"
[[runtime.mounts]]
dst = "/tmp"
isolation = "tmpfs"
size = "1GiB"

These are role-level defaults that workspaces inherit unless overridden by a workspace mount on the same dst. Useful so a Java role can declare its Gradle hygiene once and every workspace gets the right behavior.

  • Extend the mount isolation enum to shared | worktree | tmpfs | ephemeral.
  • TOML schema extension for dst-only mounts (no src).
  • Optional size field on tmpfs.
  • Docker translator for both new modes.
  • Per-instance volume label scheme (jackin.instance=<container>, jackin.dst=<slug>) so cleanup can enumerate.
  • Agent-class-level mount defaults via [[runtime.mounts]] in jackin.role.toml; workspace mounts override on dst.
  • purge cleans up Docker volumes labeled with the instance name.
  • Docs update for mount isolation, the Mounts guide, and the architecture reference.
  • tmpfs size enforcement beyond Docker’s default behavior. Use Docker’s, document the limit.
  • ephemeral mode with persistent behavior across purge (operator-managed volume name). Out of V1; if needed, add a fifth mode volume = "<name>" that doesn’t auto-clean.
  • File ownership / mode flags on tmpfs (mode = "0700"). Defer until a use case surfaces.
  • Cross-instance shared ephemeral volumes (“all my Java agents share one Gradle daemon dir”). Out of V1; the use case is handled by shared-cache mounts under shared cache pattern (a shared ~/.gradle mount overlaid on an isolated source tree).
  • Naming. ephemeral is descriptive but not great. Alternatives: instance (it’s per-instance), volume (it’s a Docker volume), private (it’s not host-visible). Recommended: ephemeral — matches the operator’s mental model (“this is gone when I purge”).
  • Default tmpfs for /tmp. Should every role get /tmp as tmpfs by default, or stay opt-in? Recommended: opt-in for V1 — defaults can change downstream after seeing real usage.
  • Interaction with worktree isolation. A workspace with a worktree-isolated source mount and a sibling ephemeral mount should compose cleanly (parent-mount-before-child rule). Confirm the existing collapse/ordering logic handles a no-src child mount correctly.
  • Is this its own leaf or an extension to mount isolation? This leaf treats it as a Phase 1 standalone item because it ships independently and the design doesn’t require any per-mount- isolation refactor. The Workspaces and Mounts guides get notes pointing here once this is implemented.