Relationships
#3691 v3 namespaces: cross-namespace reads return the reader's own content, a namespaced local tier is overwritten by solo commits, and namespaces can collide with swamp's own .swamp/ directories (bug hunt group 7)
Opened by stack72 · 10/20/2026· Shipped 10/21/2026
Background
swamp is reworking its datastore layer (tracking: swamp-club#2865). On a v3 datastore each namespace's records are materialised under their own cache root in every checkout: v3CacheRoot(repo, ns), which is .swamp/<ns>, or .swamp/ for the solo namespace "". A checkout's local tier (subdirectories its directories setting keeps out of the datastore) belongs to that checkout and is never namespaced.
A v3 bug hunt (2026-10-16) found that namespace cache roots collide with each other, with the local tier, and with swamp's own .swamp/ directories, and that cross-namespace reads use the wrong root. These are five findings with one theme. The code they cite is still present on current main; line numbers have moved since #3617, so the function names are given. Reproduce each against main first. The hunt's repro files aren't in the repo, so rebuild them from the descriptions below.
The bugs
1. Cross-namespace reads return another namespace's content, or nothing (V3-04, S2).
- Reproduced:
- in checkout
alpha,data.latest("beta:m1", "state")anddata query 'namespace == "beta"'return a record labelledbetawith alpha's attributes ({who: "alpha"}) when the model ids are shared; - with different ids, they return empty attributes even though
.swamp/beta/data/…/rawexists in alpha's checkout.
- in checkout
- Why:
fromRow(src/domain/data/data_record_mapper.ts) reads content withdataRepo.getContentSync(type, modelId, name, version)on the reader's own namespace path and ignoresrow.namespace;hydrateForeignRecord(data_query_service.ts) returns early when attributes are non-empty, and otherwise relies onfetchForeignContent, which v3 filesystem datastores never wire;- the read model puts every namespace's rows into each checkout's catalog, so foreign rows are always there.
- Trigger: one repo per namespace (staging/prod) sharing definition ids, with a workflow or CEL expression reading
data.latest("prod:model", …)from staging. The values come back silently wrong.
2. A namespaced checkout's local tier aliases the solo namespace's cache root (V3-06, S1 narrow).
Reproduced:
- checkout B (namespace
alpha,directorieswithoutworkflow-runs) ran nothing, yetfindAllGlobal()returns the solo checkout's run; - B's local-only
m1/statev1{who: "alpha"}is overwritten on disk by the solo commit{who: "solo"}; getLatestRecordin alpha returns{who: "solo"}.
B's local data was never committed, so the loss is permanent.
- checkout B (namespace
Why:
- the v3 path resolver (
default_datastore_path_resolver.ts) puts the local tier at.swamp/<subdir>, with no namespace; v3CacheRoot(repo, "")is.swamp/, the same directory;- the read model materialises every namespace's keys without checking this checkout's
directoriesfilter, and ownership is keyed by(namespace, key), so B's local file has no owner and is overwritten.
- the v3 path resolver (
Trigger: a namespaced checkout using
datastore setup --directorieson a v3 datastore shared with a solo checkout of the same repo, so with the same definition ids.
3. A symlinked .swamp/data or .swamp/outputs wedges that checkout (V3-25, S3). The read model's path guard (sqlite_read_model.ts, the PathTraversalError in its target resolution) uses realpath(.swamp) as the boundary, while the writer's assertSafePath uses .swamp/data. With a symlinked subdirectory, the commit lands and then every catch-up in that checkout throws. Only the symlinked checkout is affected, not the others.
- Is it still a wedge after #3617? That made damaged chunks a per-key failure. Check whether a path error still fails the whole catch-up.
4. Namespace names that match swamp's own .swamp/ directories can be claimed (V3-31, S3). RESERVED_NAMESPACE_NAMES (src/domain/data/namespace.ts) is DEFAULT_DATASTORE_SUBDIRS ∪ ALWAYS_LOCAL_SUBDIRS. It omits the rest of SWAMP_SUBDIRS (definitions, workflows, vault, datastore-bundles, …). Reproduced:
- with namespace
vaultordefinitions,repo upgrade(migrateFromSymlinks) moves the namespace's records into the git-trackedvaults/ormodels/directories in every checkout; - with
datastore-bundles, extension bundle eviction deletes the namespace's files.
5. A reserved namespace accepted by v3 setup makes solo recovery delete committed files (V3-32, S3).
- Why:
setupV3Filesystem(src/libswamp/datastores/setup.ts) has noisReservedNamespaceNamecheck (the v2 path has one), andrestoreNamespaceaccepts reserved names. - Reproduced: with namespace
data, its commits materialise under.swamp/data, the solo namespace's data directory. A solo checkout's recovery sweep (#sweepUnownedfor namespace"") then deletes them as unowned, for good.
What to do
Cross-namespace reads (1): resolve a foreign row's content from
row.namespace, using the local materialisation underv3CacheRoot(repo, row.namespace), for example a local foreign-content fetcher wired for v3. Never read the reader's own path when namespaces differ. Key the hydration on the namespace, not on whether the attributes are empty.The local tier vs the solo cache root (2). Pick one, and say which:
- (Recommended) refuse, at setup and at v3 open, a namespaced checkout on a datastore that also has the solo namespace, when its
directoriesomit a materialised subdirectory. That's the narrowest change, beside the existing directories check inv3_repo_context.ts. - or never materialise another namespace's key whose path lands in this checkout's local tier.
Either way, a local-only file is never written by another namespace's commit. Check the solo-with-
directoriesvariant the verifier flagged: two solo checkouts with differentdirectories.- (Recommended) refuse, at setup and at v3 open, a namespaced checkout on a datastore that also has the solo namespace, when its
Reserved names (4, 5):
- reserve every first-level
.swamp/name swamp owns, derived fromSWAMP_SUBDIRSso new directories are reserved automatically; - refuse a reserved name in v3 setup (filesystem and bucket), in
namespace set, and at v3 context open for an existing binding, with the existing reserved-name message; - make
migrateFromSymlinksand bundle eviction skip namespace cache roots; - make the solo recovery sweep skip other namespaces' cache roots.
- reserve every first-level
Symlinked subdirectories (3): treat a resolved path outside the boundary as a refused key (logged once, skipped), not a retried apply error; or align the writer's and the read model's boundaries (realpath per subdirectory). Prefer refusing a symlinked materialised subdirectory at v3 open with a clear message, so the writer never commits what the reader can't apply.
Docs: update
design/enablers/datastores.mdon namespace cache roots, the reserved names, and the local tier.
Tests
Each must fail on main first. Two checkouts on one v3 datastore in-process (filesystem; one bucket case where the path differs).
- 1: alpha reads
beta:m1with shared and with distinct ids, and gets beta's content, through bothdata.latestanddata query. - 2: a namespaced checkout with a local-only subdirectory beside a solo checkout is refused (or keeps its local file byte-for-byte after the solo commit, per the option chosen). Add the solo-with-
directoriesvariant. - 3: a symlinked
.swamp/datais refused at open, or its key is skipped without wedging. Other checkouts are unaffected. - 4: namespaces
vault,definitions,workflows,datastore-bundlesare refused at setup andnamespace set.repo upgradeand bundle eviction leave an existing namespace cache root alone. - 5: v3 setup refuses namespace
data. A solo recovery never removes another namespace's committed file. - A property test: no namespace name
namespace setaccepts mapsv3CacheRootonto a swamp-owned.swamp/directory. - swamp-uat passes unchanged.
Severity: S1 (narrow: permanent loss of local-only data) and S2 (silently wrong cross-namespace reads); the rest S3. It must land before v3 is dogfooded (datastore rework Phase 4 I).
Blocked by: nothing (swamp-club#3617 has shipped). The recovery-sweep scope issue (bug hunt group 8, not filed yet) also changes sqlite_read_model.ts, and follows this one.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.