Ephemeral Mount Modes (`tmpfs`, `ephemeral`)
Design proposal for two new per-mount isolation modes — tmpfs and ephemeral — that cover storage kind rather than git semantics. Tracked as an open roadmap item: Ephemeral mount modes.
Problem
jackin❯ 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~/.sshshadow 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.
Why It Matters
- Parallel agents share host caches today. Two agents both running
gradle buildrace on~/.gradle/daemonbecause 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.
/tmpshould default to tmpfs. Many roles bake configuration assuming/tmpis fast and ephemeral. jackin❯ default/tmpis 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.
Inspiration in multicode
Sources:
- README — Isolation (mounting policy and Apple-container subsections describe per-workspace isolation rules)
- Config —
config.toml[isolation]block — theisolated = [...]andtmpfs = [...]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.
Recommended Shape
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.
Schema extension
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, already shipped). Validation:
tmpfsandephemeralrejectsrc(no host source).tmpfsandephemeralrejectreadonly(writability is their whole point; if you want read-only, you wantsharedwithreadonly = true).tmpfsaccepts an optionalsizefield with the same parsing rules as declarative resource limits.
Backend translation
- Docker (today):
tmpfs→--tmpfs <dst>:size=<n>,mode=1777.ephemeral→--mount type=volume,source=jackin-<container>-<slug>,target=<dst>, with the volume created on firstloadand deleted bypurge(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):
--tmpfsfor both modes; the ephemeral case is bwrap's natural shape.
Role declarations
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.
Defer
tmpfssize enforcement beyond Docker's default behavior. Use Docker's, document the limit.ephemeralmode with persistent behavior acrosspurge(operator-managed volume name). Out of V1; if needed, add a fifth modevolume = "<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
~/.gradlemount overlaid on an isolated source tree).
Open Questions
- Naming.
ephemeralis 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
tmpfsfor/tmp. Should every role get/tmpas 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-
srcchild mount correctly. - Is this its own leaf or an extension to mount isolation? This is treated as a 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.
Related work
- Ephemeral mount modes (roadmap item)
- Agent Orchestration Program
- Per-mount isolation — the existing modes; this proposal extends the enum vocabulary
- Selectable sandbox backends — cross-backend translation home
- Declarative resource limits — shares the size-parsing helper