loopat architecture

A loop = context + AI + workdir, bound together in a per-loop bwrap sandbox. Every path the agent sees is composed from a few host-side sources. This is the map.

Operator host
~/.dashscope/config.json
workspace mounts (any host path)
host-secrets/<user>/
deploy-key, git-crypt.key
not bound into sandbox
Knowledge team git · ro
context/knowledge/<docs>/
.loopat/claude/CLAUDE.md — L2 doctrine
.loopat/claude/skills/ — team skills
.loopat/claude/claude.json — team mcp
.loopat/sandboxes/<name>/ — toolchain catalog
Notes team git · rw
inbox.md — workspace scratch
focus/<name>.md — task trees
memory/MEMORY.md — team memory (deliberate)
auto-commit on save
Personal user git · git-crypt
memory/ — personal mem (SDK auto-recall)
.loopat/config.json — providers, mounts, shell
.loopat/vaults/ — catalog (hidden in sandbox)
default/ baseline
dev/ prod/ test/ … exactly ONE selected per loop
Chat separate
threads with their own agent
no workdir, no sandbox
on /spawn-loop:
thread.jsonl → new loop's
/context/chat/<id>/ (ro)
composed into one mount namespace by bwrap
Sandbox (one per loop, ephemeral) paths visible to the agent
rw/loopat/loop/<id>/
this loop's ephemeral instance
workdir/ — git worktree, branch loop/<slug>-<id6>
.claude/ — SDK session state
.claude/CLAUDE.md ◀ ro-bind L2 from knowledge
.claude/skills/ ◀ ro-bind from knowledge
ro/loopat/context/knowledge/
team git, read-only inside sandbox (AI never edits)
rw/loopat/context/notes/
team git, auto-commit on save
inbox · focus · memory/MEMORY.md
rw/loopat/context/personal/
user's personal git, auto-commit
memory/ — personal memory
.loopat/config.json — per-user config
.loopat/vaults/tmpfs catalog hidden
.loopat/vault/overlay selected vault's files
rw/loopat/context/repos/<name>/
workspace's registered code repos; worktree origin
ro/loopat/context/chat/<id>/
only when spawned from a chat thread — seed history
tmpfs$HOME
fresh tmpfs + member-mounts re-overlaid
e.g. .loopat/vault/.ssh$HOME/.ssh
ro/usr /etc /lib /bin /sbin
host system paths (so binaries work). /tmp shared.
◆ Agent — driving this loop

▼ READ sources concatenated each turn

L1 doctrine bundled templates/CLAUDE.md
L2 team knowledge/.loopat/claude/CLAUDE.md
L3 project workdir/CLAUDE.md
L4 runtime server-computed (id, title, vault, sandbox)
skills team knowledge/.loopat/claude/skills/
mcp tools team claude.json
personal mem SDK auto-recalls each turn
team mem reads MEMORY.md on complex turns
credentials /personal/.loopat/vault/* (one vault only)
chat history /context/chat/<id>/ (if spawned from chat)

▲ WRITE destinations (server auto-commits)

workdir/* auto-commit on loop branch
notes/inbox append to team scratch
notes/focus task trees
notes/memory agent auto-promotes from personal when team-relevant
personal/memory SDK-managed observations
/vault/* rare — credential helper refresh
never writes knowledge/, other repos/<x>

distillation — knowledge condenses upward

personal/memory/
private observations
auto-promote
or curate
notes/memory/
team-shared, deliberate
distill
loop
knowledge/
canonical, human-reviewed
and separately: loop/workdir/repos/<name>/ (merge the loop's branch back)
ro read-only bind — agent sees, cannot write
rw read-write bind — agent's writes hit disk
tmpfs empty fs mounted on top to hide what's underneath
overlay multiple host files composed into one sandbox path

Loop = Sandbox × Vault

The one thing to internalize. Two orthogonal axes, picked independently at spawn.

Sandbox

"what tools the loop can use"
owned by: admin / team (knowledge git)
contains: mise toolchain · shell · mcp servers
versioned by: knowledge git commit
frontend — node, bun, jira-mcp, figma-mcp
backend — go, python, redis-cli
sre — kubectl, aws-cli, datadog-mcp
×

Vault

"with what credentials it runs"
owned by: member / individual (personal git, git-crypt)
contains: apiKey · ssh · tokens · provider keys
isolated by: bwrap overlay (other vaults invisible)
default — shared baseline
dev — dev env credentials
prod — prod credentials (you specifically have)

Read path — what the agent learns from

LayerSource on hostLoaded byScope
L1 doctrineserver/templates/CLAUDE.mdsystem-prompt builder, always-onsandbox basics, path conventions
L2 teamknowledge/.loopat/claude/CLAUDE.mdro-bind to .claude/CLAUDE.md, SDK loadsworkspace conventions
L3 projectworkdir/CLAUDE.mdSDK loads from cwdrepo-specific conventions
L4 runtimeserver-computed (id, vault, sandbox, …)concat into system promptper-turn variables
skillsknowledge/.loopat/claude/skills/ro-bind to .claude/skills/, SDK discoverscallable procedures
mcpknowledge/.loopat/claude/claude.jsonpassed to SDK at spawnjira, github, datadog, …
personal memorypersonal/memory/SDK auto-recall (per .claude/settings.json)your habits, user facts
team memorynotes/memory/doctrine tells agent to read MEMORY.mdgotchas, conventions
chat threadchat/<tid>/history.jsonlro-bind into sandbox if loop spawned from chatseed conversation
credentialspersonal/.loopat/vaults/<v>/walked & overlay-mounted at .loopat/vault/apiKey, ssh, tokens

Write path — where the agent's output lives

PathPersistenceNotes
workdir/*auto-commit on loop/<slug>-<id6>the loop's actual work product
notes/inbox.mdauto-commit to team notes gitappend-only scratchpad
notes/<focus>.mdauto-commitsmall markdown task trees
notes/memory/<name>.md + indexauto-commitagent auto-promotes from personal when topic is workspace-wide
personal/memory/<name>.mdSDK-managed, auto-commitprivate observations
/vault/*git-crypt encrypted at commitrare — credential rotation paths
knowledge/NEVER — flows back via distillation
repos/<x>/NEVER directly — only via the loop's workdir/

Boundaries the sandbox enforces

Agent attempts to …What stops it
read another user's secretspersonal/<other-user>/ isn't bound into this sandbox at all
read another vault's keyshost-side .loopat/vaults/ is tmpfs'd; only selected vault overlays as /.loopat/vault/
escape via a symlink in the vaultwalkVaultFiles checks realpath against personal/<user>/ and refuses targets outside
modify team knowledgeknowledge/ is ro-bind; writes return EROFS
commit into a sibling repo's mainlinerepos are rw but workflow rules + worktree-branch isolation steer commits onto loop/… only
see the host filesystem outside /loopatsandbox root is fresh tmpfs; only explicitly-bound paths exist

Why this shape

Filesystem-first, no DB

Every artifact is a file. State of a loop, vault contents, memory, branch — all ls-able. No "what does the DB say" debugging.

Loop ephemeral, context persistent

/loopat/loop/<id>/ dies with the loop. /loopat/context/ survives — branch + memory + notes remain.

Capability ⊥ identity

Sandbox × vault. Same engine powers "alice testing the frontend" and "carol fighting a prod fire" — different cells of the matrix.

Read down, write up — slowly

Knowledge flows downward (consumed). Writing back to knowledge/ takes a distill loop, not a one-line append. Friction is intentional.

Sandbox is the membrane

Nothing crosses implicitly. Every path the agent sees is a --bind line in buildBwrapArgs. The host can sleep through misbehavior.

Where to look in the code

ConceptFile(s)
sandbox composition (buildBwrapArgs)server/src/bwrap.ts
vault catalog + symlink validationserver/src/vaults.ts
loop lifecycle + auto-initserver/src/loops.ts
L1 doctrine (bundled)server/templates/CLAUDE.md
memory recall configserver/src/loops.ts (.claude/settings.json per loop)
auto-commit on writesserver/src/workspace.ts (vaultWrite)
chat → loop spawnserver/src/chat.ts
sandbox toolchain specserver/src/sandboxes.ts, knowledge/.loopat/sandboxes/<name>/