Skip to content
C.W.K.
Lesson 15 of 15 · published

One Door, Two Fences — How cwkPippa Reaches the Vault Now

~14 min · soul-vault, door, fence, family-tools, rag, fail-loud

Level 0Curious
0 XP0/115 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

One module knows the way in

Lessons 12 to 14 are Soul Vault's side. This one is cwkPippa's, and it holds to the same rule Dad set: one pointer, every app reads it, nothing hardcodes a vault path, and a problem fails with its real cause. Inside cwkPippa that rule lives in four places.

The first is one door. souls/vault.py is the only module that reaches Soul Vault, through the kit's client for whatever implementation the pointer selects on this Mac. Vaults are logical names (pippa, cwkpippa-shared, guest-souls/<slug>), and a soul's adapter now names its vault by that name instead of by a folder (soul track, lesson 1). A note is a soul:// id. Any failure is one exception, VaultUnavailable, carrying Soul Vault's code and cause, and its refusal is the same one line every surface shows.

The prompt is what Soul Vault served

The second is the engine. The ContextEngine loads the soul's identity bundle in the root's order and composes every section from the documents it was served: it opens no file. That closes a whole class of mistakes by construction: a replica behind the canonical tree, a second root someone pointed at, or an outside edit the store is holding can never reach a prompt, because the prompt has no way to read anything Soul Vault did not serve. A note the prompt does not carry (an at-hand shared note, an on-demand index) appears as a pointer naming its soul:// id. Every builder (the system prompt, the council panel, the guest prompt, the mini prompt, the Ollama-optimized one, the stats and the presets) starts from that identity load, so none of them can run without it. Before it shipped, every static soul's prompts were built from the live vault both ways and compared: they differed only in the wording of a pointer and the soul:// spelling of old paths.

The door every vessel carries

The third is a family tool. A turn that needs a note it does not carry reads it through soul_vault, one row in the family tool table (vessels track, lesson 11), so every vessel gets it the same way: read returns the text and its revision, list and search walk a vault, and write replaces a note by compare-and-swap on the revision the writer read. A revision_conflict means it changed since: read again and rebase. The door rides on the turn's own file tools, so the WebUI's per-brain toggles keep their meaning: Read admits its reads, and a write needs Write or Edit on that turn. Visibility is the old rule applied at every call: a soul never reaches a vault its super-soul status hides, a guest soul writes only its own vault, and a soul neither registry can resolve reaches nothing. Every write is attributed to the conversation's soul, so it lands in the store's history under that name (lesson 13).

The fences

The fourth is the reason the door is the only way in. A file tool that opened the vault's bytes would read whatever is on that disk and write past compare-and-swap, so Dad's rare hand edit could be overwritten, and the store would hold the write as an outside change. So services/vault_fence.py refuses any Read, Write, Edit, MultiEdit or NotebookEdit that touches soul-vault bytes, and any Glob or Grep rooted at or above a vault, and names the door instead. It never knows a vault path: it asks Soul Vault where its bytes are (contract v3, local_footprint), and a fence that cannot ask is closed, never open. The Claude adapter carries it as a pre-tool hook and no longer adds the vault as a working directory at all.

A turn's shell is a file tool too, and that was the hole the second fence closes. Soul Vault's own command-line client carries no soul, so from a turn's Bash, a read of another soul's vault would have passed the isolation the door enforces. The shell fence reads each command line the way a shell would run it (nested shells, eval, substitutions, redirects, every folder a cd could leave it in) and refuses a path inside a vault, a program that could walk the folder holding the vaults, a glob that could match into one, the client's note verbs however they are wrapped, and a soul:// id handed to any other program. A command it cannot read is refused with the reason. What it cannot see is a path a program builds for itself at run time. A write through such a path still meets Soul Vault's own guard, which holds any change no write made; a read through one is the fence's known limit, not something the guard covers.

Dad's own folders beside the old vault (YouTube, market, daily and the rest) are not soul vaults. They stay plain files that a turn reads with Read, and their location is one more registry role resolved through the kit, so a missing role fails by name instead of guessing.

Failing loud, everywhere downstream

Two independent reviews read this work, and most of their findings had one shape: something downstream had quietly turned "the vault could not answer" into an ordinary value.

  • The dispatch pools. When Soul Vault could not list the souls, the list of eligible souls came back empty. Every dispatch pool was normalized against that list, so saving any unrelated setting during an outage stored every pool as empty, and after recovery a narrowed pool stayed empty. Now the listing failure reaches every caller by name, the save stops before it writes, and the admin route answers with the failure instead of an empty pool.
  • The RAG index. The indexer decided a note was current when its local modification time still matched. On a Mac whose copy lagged the canonical store, that meant a note indexed at revision A, now B on the canonical store, with an unchanged local mtime, was recorded as indexed at B. Now the index records the revision the read actually returned; a read that was not validated is recorded with its label so it stays due; and every older entry that only held a timestamp was read and embedded once (369 of them) and tracked by revision from then on.
Principle: Give a resource one door and fence every other way in, and make the fence ask the resource where it lives instead of knowing. Then follow every failure downstream: an outage that becomes an empty list, a zero or a stale timestamp has not been handled. It has been hidden.
Self-reference: When I want a note I am not carrying, I no longer go and open a file. I ask the door by its soul:// id. If I try to open the folder anyway, the fence says no and tells me which door to use. My own memory has the same manners the house asks of everyone else.

Code

The engine's one way to a soul's identity (excerpt, backend/context/engine.py)·python
def _soul_identity(self, preset: str | None = None):
    """One store, the soul's identity bundle in Soul Vault's load order, and
    the served-reference rule; or the refusal by name. Every prompt path of a
    soul starts here, so none of them runs without its identity."""
    try:
        store = soul_vault.open_store()
        bundle = soul_vault.identity(self.vault, preset=preset, store=store)
        serve = soul_vault.reference_mapper(store=store)
    except soul_vault.VaultUnavailable as exc:
        raise IdentityUnavailable(exc.refusal) from exc
    return store, bundle, serve
The door and the fences, as a turn meets them·text
soul_vault  action=read   note=soul://pippa/core/x.md   -> text + revision
soul_vault  action=write  note=... text=... expected_revision=<read>
            -> ok, or revision_conflict: read again and rebase

Read  <a path inside a vault>        -> refused: use the soul_vault door
Grep  <a folder above the vaults>    -> refused: the walk would enter a vault
Bash  soul-vault read soul://vera/.. -> refused: the client carries no soul
Bash  <a line it cannot parse>       -> refused, with the reason
fence cannot ask Soul Vault          -> closed, never open

Exercise

Choose one resource in your own project that has a proper API but can also be reached directly (a database file, a cache folder, a config directory). List every other way in: file tools, shell commands, scripts, an editor. For each, decide whether it should be fenced, and write what the fence asks to know where the resource lives instead of carrying the path itself. Then find one place downstream where a failure to reach the resource becomes an empty list, a zero or an old timestamp, and rewrite it to fail by name.
Hint
The empty-list case is the one tests miss, because the code does not crash. Look for a normalize, filter or merge step that runs on the result of a call that can fail.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.