Internals¶
agedum compiles source into a throwaway directory and binds required files at their harness-visible targets under bubblewrap, so the compiled files appear at their expected paths for that process — and only that process. Compiled context stays in temporary staging; writable provider seeds and mountpoint preparation are deliberate persistent filesystem writes, described below.
The launch pipeline¶
flowchart TD
a["load_source() + load_global_source()"] --> b["compile_claude / compile_kimi / compile_opencode / compile_cline / compile_reasonix / compile_aider / compile_pi / compile_codex<br/>→ Plan(binds, extra_args, safe_overrides)"]
b --> c["assert_safe(): refuse git-tracked targets"]
c --> d["bwrap --dev-bind / / --tmpfs shadow … --ro-bind src target … -- command extra_args"]
d --> e["child runs, sees injected files"]
e --> f["sweep stub mountpoints bwrap left behind"]
The main modules are:
sources.py— locates the project root and the project/global source files into aSource(root,agents_md,skills_dir).harness.py—compile_claude/compile_kimi/compile_opencode/compile_cline/compile_reasonix/compile_aider/compile_pi/compile_codexrender aSourcepair into aPlan: a list of absolute(compiled-file → mount-target)binds, plusextra_argsto append to the command, plussafe_overrides— targets to shadow with an empty tmpfs instead of binding content. Pi does not shadow source skills: it uses exact native exclusions in an untracked project.pi/settings.json, leaving source files and the shared Git index visible (see Pi).launcher.py—assert_safe,build_bwrap_argv, andrun_virtualfsvalidate, compose thebwrapargv, run the command, and clean up.proxy.py— launch-bound localhost reverse proxies (the child reaches them at127.0.0.1, shared into the bwrap namespace):FoldProxyfolds Anthropic system messages,TranslateProxytranslates Anthropic ↔ Chat Completions,ResponsesToChatProxytranslates Responses ↔ Chat Completions, andFailoverProxyroutes OpenCode model chains. Every local proxy requires its own runtime-only launch capability; loopback alone is not authorization.
Provider mode adds one more injection channel on top of the same pipeline:
Launch.config_files — agedum-generated config files a harness needs on disk
(reasonix's project-root reasonix.toml; pi's user-scope models.json /
settings.json, deep-merged onto any existing file). The CLI writes each into the
throwaway dir and appends it to plan.binds, so generated configs go through the same
git-safety check and stub sweep as every other bind. A config file flagged writable
(Kimi's custom-endpoint config.toml or Cline's providers.json) is atomically replaced at
its real target, created owner-only before any secret bytes are written, and rejected if its
parent or target is unsafe. It has no --ro-bind, so harness rewrites remain possible.
Dry-run uses the same plan construction as runtime and computes prospective grants and
read-only generated-config binds without creating a seed.
The compiled tree lives under a tempfile.mkdtemp() directory that is removed when the
command exits.
To see the Plan for a given source without launching — the exact mount targets the
harness will read — run --dry-run: agedum <name> --dry-run (provider mode) or
agedum --wrapper <harness> --dry-run -- <cmd> (wrapper mode). It compiles the source,
prints the bind targets and any appended args, and stops before the bwrap step.
The mount namespace¶
The bwrap argv starts by mirroring the whole real filesystem read-write into the
namespace, then read-only-binds each compiled file over its target:
Because the binds use absolute targets, the same mechanism places project-scope
files inside the tree (./CLAUDE.md) and global-scope files under the user config dir
(~/.claude/...). A --ro-bind masks any pre-existing file or directory at the target
for the duration of the run, and the mask is visible only inside this namespace — other
processes, and your shell after the command exits, see the original tree.
Directory binds are overlaid one level deep, not masked wholesale. The only directory
binds agedum emits are skill trees, and binding the whole compiled dir over
~/.config/opencode/skills/ (or ~/.claude/skills/, ~/.kimi-code/skills/) would hide any
hand-authored skill already living there. So build_bwrap_argv expands a directory bind
into one --ro-bind per child — <compiled>/<name> over <target>/<name> — leaving
unrelated siblings in the real target dir visible. A shipped skill still wins over a
same-named on-disk one (its child bind overlays that subdir); only folders agedum does
not ship survive. File binds (CLAUDE.md / AGENTS.md) pass through unchanged.
This is why agedum is harness-agnostic at the launch layer: every agent CLI ultimately just reads files, and the namespace makes the compiled files be those files.
Safety¶
Two rules, both validated empirically and not to be regressed:
No git-tracked targets¶
The namespace shares the project's real, shared .git directory — it is not
masked. So a git add / git commit run inside the namespace writes to the real
repository. If agedum overlaid a git-tracked file (say a real CLAUDE.md you keep in
the repo), injected content could be committed by accident.
assert_safe therefore refuses to inject over any git-tracked path. Targets must
be untracked; keep them gitignored as an operator prerequisite. Git ownership is queried
from each target's actual enclosing worktree, independently of instruction-source discovery.
This covers nested projects and global targets inside other repositories, and unexpected
Git errors refuse the launch. In practice: list CLAUDE.md,
.claude/, .kimi-code/, .opencode/, .cline/, .reasonix/, .pi/, and .codex/ in your .gitignore.
The check runs over the effective, per-child binds — the exact paths the namespace
will mount. A tracked but unrelated sibling inside a skills target dir (say a
hand-authored skill you deliberately version under .claude/skills/) does not block the
launch, because the per-child overlay never masks it; only a path agedum would actually
bind over must be untracked. safe_overrides receive the same check: hiding a tracked
file can stage a deletion in the shared index even when the shadow is read-only.
Stub sweeping¶
To bind a file at a path that does not yet exist, bwrap creates the mountpoint on the
real filesystem first. After the namespace exits, those mountpoints remain as empty
stubs (a 0-byte file, or an empty directory) — the injected content never leaks,
but the empty placeholder can.
run_virtualfs records which candidate paths existed before the run, and after the
command sweeps the ones it created — deepest first, and only if still empty. Candidates
are each target and its immediate parent, taken over the dir-level binds (e.g.
the .claude dir created to hold .claude/skills), the per-child overlay targets
(e.g. .claude/skills/<name> — the empty stub left when agedum ships a skill the target
dir did not already have), and the safe_overrides tmpfs shadows (bwrap creates their
mountpoints the same way). Anything that pre-existed — including a user's
~/.config/opencode/skills/ that already held skills — is left alone. Directories prepared
before this snapshot (exact injection parents and harness state dirs under a sandbox) can
remain, as can writable provider seeds and files the harness creates. Stub sweeping is not
a guarantee of a clean or untouched working tree.
Adding a harness¶
A new harness is a single compiler function compile_<harness>(project, global_, dest)
-> Plan. It renders the source however that harness expects and returns binds and/or
extra_args. Register it in the CLI's _COMPILERS table under its --wrapper <harness>
name. The harness need not fit the pure path-discovery shape: the aider harness
injects instructions at read-only bound cache paths named by --read flags, and skips
skills entirely — both expressed through the same Plan(binds, extra_args) return. The
launcher and safety rules are shared, so a new harness inherits the namespace, git-safety,
and cleanup for free.