Skip to content

feat(container): own the root - checkRoot facts, layout helpers, bundle + destroyUser - #47

Merged
vreshch merged 1 commit into
masterfrom
feature/root-health
Aug 23, 2026
Merged

feat(container): own the root - checkRoot facts, layout helpers, bundle + destroyUser#47
vreshch merged 1 commit into
masterfrom
feature/root-health

Conversation

@vreshch

@vreshch vreshch commented Aug 23, 2026

Copy link
Copy Markdown
Member

Why

The memory host still opens node:fs for everything ABOVE one vault: is the store root there, is it writable, is it the right volume, how full is it, where does <userId>/<vault>.git live, export a memory, erase an account. That is engine knowledge living in the host. This PR moves all of it in, so a host can reach zero node:fs imports for store concerns.

Specced by the read-only review in ~/vaults/agentage/research/code-review-2026-08.

API surface added

src/container/root-health.ts

Export Shape
checkRoot(root, opts?) Promise<RootFacts> - opts: { markerFile?: string; probeWrite?: boolean }
RootFacts { reachable, writable, markerPresent: boolean | null, diskFreeBytes, diskTotalBytes }
CheckRootOptions the options type
checkRootWritable(dir) unchanged 1.x signature, now a projection over checkRoot (moved here from git-admin.ts)
  • Never throws. A vanished root is a fact: all-false + zeros, so a probe reports it instead of failing the request that asked.
  • Cheap by default - access(R_OK|W_OK); health endpoints poll forever. probeWrite: true opts into the honest write+unlink probe (bits lie on a full disk or a read-only remount).
  • markerFile catches the writable-but-WRONG volume; markerPresent is null when none was asked for, never a silent false.
  • Disk numbers from statfs (bavail, i.e. what an unprivileged writer may actually use).

src/container/layout.ts - REPO_SUFFIX, userDir(root, userId), vaultRepoDir(root, userId, vault), tombstoneRepoDir(root, userId, vault, stamp). Every segment goes through isSafeSegment first, so a hostile name throws invalid_path rather than producing a path. vault-container.ts has no inline join left - the tombstone name is a helper too, and stamps keep their own rule (dots and colons for timestamps, never a separator).

Two container verbs (so the host stops holding the root at all):

container.bundle(access, vault); // Promise<Buffer | null>
container.destroyUser(access, userId); // Promise<boolean>
  • bundle is gated exactly like open() (invalid_path -> forbidden -> path), then delegates to bundleRepo semantics against vaultRepoDir. null = nothing to export (no repo, no commits, or a tombstoned name) - an absent vault of another account never reads differently from an empty one of your own.
  • destroyUser mirrors how remove() gates: validate segments, then the identity gate (any userId but access.userId is forbidden, whether or not that account exists), then canDelete. It disposes every live object it wipes, sweeps tombstones with the rest, and maps an unreadable account dir to unavailable rather than a silent erase. No stamp - remove tombstones, destroyUser erases.

RoutedContainer (new exported type) - Pick<VaultContainer, 'list' | 'open' | 'create' | 'remove'>, now the parameter type of createRouter. Export and account erasure are not routing concerns, so adding container verbs does not widen what a router (or a double standing in for one) must provide. This is why the conformance kit's inline router double needed no edit.

Conformance evidence

  • contractSuite + securitySuite unchanged - not one line touched in src/conformance/, and nothing landed on VaultStore. The new surface is container-level only.
  • New units: test/container/root-health.test.ts (reachable / vanished / not-a-dir / read-only in both probe modes / marker present-absent-null / 0 < free <= total / unreachable = all-false, no throw / the default writes nothing to disk), test/container/admin-verbs.test.ts (bundle clones back out through git clone; null for unknown, empty, tombstoned and other-account; access matrix granted/denied/hostile; destroyUser disposal + idempotence + cross-account refusal + canDelete + unavailable on an unreadable dir), plus layout assertions extending test/container/bare-git-layout.test.ts (what the container writes IS what the helper computes; the whole HOSTILE_IDS corpus refused).
  • smoke:dist gained asserts for checkRoot (vanished + live root) and vaultRepoDir (layout + invalid_path on traversal).
  • checkRootWritable's own case moved from test/stores/git-admin.test.ts to the new file - git-admin is strictly per-repo again.

Verify tail

npm run verify green (type-check + lint + format:check + coverage + build + dist smoke):

Test Files  26 passed (26)
     Tests  273 passed (273)

 src/container     |   97.52 |    95.83 |     100 |   98.13
All files          |   97.44 |     88.4 |   98.25 |   99.01
dist smoke ok

Container threshold is 85 across the board; every new file is under the 200-line house limit (layout 52, root-health 90, vault-container 179, git-admin down to 56).

Version

Minor (1.2.0) - additive contract, checkRootWritable unchanged. Not bumped here - the coordinator releases.

…le + destroyUser

The engine now answers every question ABOVE one vault, so a host needs no
node:fs of its own: root facts (reachable/writable/marker/disk) as values that
never throw, the <root>/<userId>/<vault>.git layout as named helpers that refuse
a hostile segment instead of returning a path, and two container verbs - export
one vault (bundle) and erase one account (destroyUser).

checkRootWritable keeps its 1.x signature as a projection of checkRoot, and moves
out of git-admin.ts, which is strictly per-repo again. createRouter now asks for
RoutedContainer (the lifecycle subset), so adding container verbs never widens
what a router double must provide.
@vreshch
vreshch marked this pull request as ready for review August 23, 2026 00:08
@vreshch
vreshch merged commit 7c84b0b into master Aug 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant