# 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

Layer | Source on host | Loaded by | Scope
--- | --- | --- | ---
L1 doctrine | `server/templates/CLAUDE.md` | system-prompt builder, always-on | sandbox basics, path conventions
L2 team | `knowledge/.loopat/claude/CLAUDE.md` | ro-bind to `.claude/CLAUDE.md`, SDK loads | workspace conventions
L3 project | `workdir/CLAUDE.md` | SDK loads from cwd | repo-specific conventions
L4 runtime | server-computed (id, vault, sandbox, …) | concat into system prompt | per-turn variables
skills | `knowledge/.loopat/claude/skills/` | ro-bind to `.claude/skills/`, SDK discovers | callable procedures
mcp | `knowledge/.loopat/claude/claude.json` | passed to SDK at spawn | jira, github, datadog, …
personal memory | `personal/memory/` | SDK auto-recall (per `.claude/settings.json`) | your habits, user facts
team memory | `notes/memory/` | doctrine tells agent to read MEMORY.md | gotchas, conventions
chat thread | `chat/<tid>/history.jsonl` | ro-bind into sandbox if loop spawned from chat | seed conversation
credentials | `personal/.loopat/vaults/<v>/` | walked & overlay-mounted at `.loopat/vault/` | apiKey, ssh, tokens

## Write path — where the agent's output lives

Path | Persistence | Notes
--- | --- | ---
`workdir/*` | auto-commit on `loop/<slug>-<id6>` | the loop's actual work product
`notes/inbox.md` | auto-commit to team notes git | append-only scratchpad
`notes/<focus>.md` | auto-commit | small markdown task trees
`notes/memory/<name>.md` + index | auto-commit | agent auto-promotes from personal when topic is workspace-wide
`personal/memory/<name>.md` | SDK-managed, auto-commit | private observations
`/vault/*` | git-crypt encrypted at commit | rare — 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 secrets | `personal/<other-user>/` isn't bound into this sandbox at all
read another vault's keys | host-side `.loopat/vaults/` is tmpfs'd; only selected vault overlays as `/.loopat/vault/`
escape via a symlink in the vault | `walkVaultFiles` checks `realpath` against `personal/<user>/` and refuses targets outside
modify team knowledge | `knowledge/` is `ro-bind`; writes return EROFS
commit into a sibling repo's mainline | repos are rw but workflow rules + worktree-branch isolation steer commits onto `loop/…` only
see the host filesystem outside `/loopat` | sandbox 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

Concept | File(s)
--- | ---
sandbox composition (`buildBwrapArgs`) | `server/src/bwrap.ts`
vault catalog + symlink validation | `server/src/vaults.ts`
loop lifecycle + auto-init | `server/src/loops.ts`
L1 doctrine (bundled) | `server/templates/CLAUDE.md`
memory recall config | `server/src/loops.ts` (`.claude/settings.json` per loop)
auto-commit on writes | `server/src/workspace.ts` (`vaultWrite`)
chat → loop spawn | `server/src/chat.ts`
sandbox toolchain spec | `server/src/sandboxes.ts`, `knowledge/.loopat/sandboxes/<name>/`