feat(container): own the root - checkRoot facts, layout helpers, bundle + destroyUser - #47
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The memory host still opens
node:fsfor 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>.gitlive, 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 zeronode:fsimports for store concerns.Specced by the read-only review in
~/vaults/agentage/research/code-review-2026-08.API surface added
src/container/root-health.tscheckRoot(root, opts?)Promise<RootFacts>-opts: { markerFile?: string; probeWrite?: boolean }RootFacts{ reachable, writable, markerPresent: boolean | null, diskFreeBytes, diskTotalBytes }CheckRootOptionscheckRootWritable(dir)checkRoot(moved here fromgit-admin.ts)access(R_OK|W_OK); health endpoints poll forever.probeWrite: trueopts into the honest write+unlink probe (bits lie on a full disk or a read-only remount).markerFilecatches the writable-but-WRONG volume;markerPresentisnullwhen none was asked for, never a silentfalse.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 throughisSafeSegmentfirst, so a hostile name throwsinvalid_pathrather than producing a path.vault-container.tshas no inlinejoinleft - 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):
bundleis gated exactly likeopen()(invalid_path->forbidden-> path), then delegates tobundleReposemantics againstvaultRepoDir.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.destroyUsermirrors howremove()gates: validate segments, then the identity gate (anyuserIdbutaccess.userIdisforbidden, whether or not that account exists), thencanDelete. It disposes every live object it wipes, sweeps tombstones with the rest, and maps an unreadable account dir tounavailablerather than a silent erase. No stamp -removetombstones,destroyUsererases.RoutedContainer(new exported type) -Pick<VaultContainer, 'list' | 'open' | 'create' | 'remove'>, now the parameter type ofcreateRouter. 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+securitySuiteunchanged - not one line touched insrc/conformance/, and nothing landed onVaultStore. The new surface is container-level only.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 throughgit clone; null for unknown, empty, tombstoned and other-account; access matrix granted/denied/hostile; destroyUser disposal + idempotence + cross-account refusal +canDelete+unavailableon an unreadable dir), plus layout assertions extendingtest/container/bare-git-layout.test.ts(what the container writes IS what the helper computes; the wholeHOSTILE_IDScorpus refused).smoke:distgained asserts forcheckRoot(vanished + live root) andvaultRepoDir(layout +invalid_pathon traversal).checkRootWritable's own case moved fromtest/stores/git-admin.test.tsto the new file -git-adminis strictly per-repo again.Verify tail
npm run verifygreen (type-check + lint + format:check + coverage + build + dist smoke):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,
checkRootWritableunchanged. Not bumped here - the coordinator releases.