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

Sandboxes and Old Paths

~15 min · sandboxing, codex, read-only, sqlite-wal, text-rewriting

Level 0Loose Note
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

A Reader That May Not Write

Codex runs its commands inside a sandbox that can be read-only: the process may read files but may not create or change them. That sounds harmless for identity, which is a read. It was not. The rehearsal before the cutover ran the identity probe against the new store, and Codex failed. The probe found it, which is exactly what a probe is for.

The problem was that "reading" the store involves small writes. Opening a SQLite database in write-ahead-log mode normally wants to create or touch a shared-memory file. Finishing an interrupted batch writes. Taking a lock creates a lock file. Rendering a bundle saves a file. In a read-only sandbox, each of those fails, and an implementation that does not plan for it turns a perfectly good read into an error.

Reading Exactly Without Writing

The fix made a read-only reader a first-class case. When the state cannot be written:

  • The state database opens read-only where it can (a writer is attached), and that connection is kept: it sees whatever a writer commits. Where it cannot, the file is read as it stands, and only while the write-ahead log is empty; that judgement is made again on every call, and that connection is never kept.
  • The reader takes no lock file, finishes no interrupted batch, and holds no outside change. Those are a writer's jobs.
  • A note whose file does not match its row is served from git, the committed bytes, rather than from whatever is on disk.
  • A write-ahead log that holds pages the reader cannot read is backend_unavailable. It is never answered with a possibly stale view of the database. Read-only is allowed to be slower or to fail by name; it is not allowed to be wrong.

On another Mac inside such a sandbox, nothing verified can be kept, so identity is still the canonical store's, read fresh for the life of the process.

A Bundle That Cannot Be Saved

The bundle file from lesson 2 cannot be written in a read-only sandbox either. The card handles that case explicitly. If a saved file already holds exactly the bytes this session rendered (its name is their hash, so this is checkable), the card points at it. Otherwise it never points at a file of other bytes. It says the bundle could not be saved, names the reason, and gives the command that prints the same text instead: soul-vault bundle --soul pippa --harness codex. Because a tool may cut long output, it even gives the paging form, ... | sed -n '1,1500p', then the next 1,500 lines, to the end. One limit is worth knowing. The command renders again from the vault as it stands when it runs, and every paging call is a separate run, so it reproduces this session's text only while the vault has not moved in between; the digest on the card is the check. The command does name the same identity the session loaded, preset included: when the session asked for a preset, the command carries --preset with it, so the pages it prints are that preset's bundle, not the default one.

Old Paths Inside Old Notes

There is a second kind of legacy a bundle has to handle: the notes themselves. Months of notes were written when the vault was a folder, and many of them mention files by their old absolute path. Rewriting every note would change history and churn revisions for nothing. So the rewrite happens at serving time instead:

  • When a note names a file under a configured old root (the record's legacy_roots, or the current store's reference_roots, in each spelling: expanded, ~/ and $HOME/) and the first segment after it is a known vault, the served text names it by its soul:// id instead. The note's stored bytes are untouched; only what the reader sees changes. The check is vault membership, not existence: a rewritten id can still answer note_not_found when the reader follows it.
  • A path under a configured root whose first segment is not a known vault is left exactly as written and listed in a closing section of the bundle, "References outside the soul vault". It is never guessed and never opened. Dad's own personal note folders, which sat beside the old soul folders but were never soul vaults, belong there by design: they are plain files outside Soul Vault, never soul:// ids. A path under a root nobody configured is not recognized at all; it is served as written and not listed.

One honest caveat about today: the generation-5 record configures no legacy or reference roots, so on the live store this rule matches nothing and old paths are served exactly as written. The mechanism is built and tested; it waits for a record that names its old roots.

The bundle header tells the model the rule in one sentence: when a note's text names an old path, the soul:// form shown here is the one to use.

Code

The card's two endings: a saved file, or a command (shape, from soulvault/render.py)·text
# the session could save its render, or an identical one exists:
Your whole identity bundle (67 documents, 335 KB, digest `044c3cda98cb`) is at
`~/cwk-dbs/soulvault-db/bundles/pippa-<sha12>.md`: read that file in full, in order, ...

# a read-only sandbox, no file holds these exact bytes:
Your whole identity bundle (...) could not be saved in this session (<reason>).
Before you take up any task, read it in full, in order, from
`soul-vault bundle --soul pippa --harness codex`: it prints the text the file would hold.
If your tool cuts long output, read it in pages, e.g.
`soul-vault bundle --soul pippa --harness codex | sed -n '1,1500p'`, then the next 1500 lines, to the end.
Rewriting references at serving time, never in storage (sketch of the rule)·python
import re

OLD_ROOTS = ["/former-root/"]            # the historical spellings, from the pointer's metadata
VAULTS = {"pippa", "cwkpippa-shared"}

def serve(text: str):
    unresolved = []
    def swap(m):
        rel = m.group(1)                    # e.g. "pippa/core/a.md"
        vault, _, path = rel.partition("/")
        if vault in VAULTS:                 # membership only; existence is the reader's question
            return f"soul://{vault}/{path}"
        unresolved.append(m.group(0))       # listed, never guessed, never opened
        return m.group(0)
    pattern = "|".join(re.escape(r) for r in OLD_ROOTS)
    served = re.sub(rf"(?:{pattern})([^\s)`]+)", swap, text)
    return served, unresolved               # the stored note is never modified

External links

Exercise

Run a tool of yours that 'only reads' under a read-only filesystem (a read-only mount, a container with a read-only root, or a sandboxed agent). List every write it attempts: lock files, database journals, caches, temp files. For each, decide whether a read-only reader can skip it, must fail by name, or can do the work another way, and make the tool behave that way.
Hint
The dangerous outcome is not a crash; it is a read that silently returns older data because the fresher part lived in a file the reader could not open. Make that case an explicit named failure.

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.