Skip to content

feat: offline space backup and restore#244

Open
davidfarah2003 wants to merge 14 commits into
mainfrom
feat/channels-export
Open

feat: offline space backup and restore#244
davidfarah2003 wants to merge 14 commits into
mainfrom
feat/channels-export

Conversation

@davidfarah2003

Copy link
Copy Markdown
Contributor

Replaces the interim channel-registry export with a complete offline backup/restore lifecycle for a space's broker-resident durable state, per the approved .internal space-backup plan.

Surface

  • cotal down --preserve-state — maintenance cut: fences the manager and Plane-3 delivery before the inventory snapshot, proves control-plane death on the wire (leases, not pidfiles), retains credentials/durables/principals, and publishes an inode-bound ready record only after every proof. Crash-recoverable at every boundary (durable prepare intent, cut-intent, cut-committed).
  • cotal backup create <dir> [--only full|registry] — offline artifact from a ready cut: attempt-owned reflink clone, watchdog-owned isolated loopback broker with exact per-phase credentials in every auth mode, canonical stream/consumer validation, no-consumer snapshots, conservative ACK-floor checkpoints, atomic manifest.json completion marker. Ownership precedes existence: the maintenance claim journals pending path slots before anything is created, and recovery deletes exactly the journaled inodes.
  • cotal up (ordinary resume) — resumes the exact preserved source inode with a bound, provable listener (adopt-alive / replace-dead / refuse-ambiguous), and relaunches agents under their retained principals, never minting replacements.
  • cotal up --restore <dir> [--restore-only registry] [--accept-missing-source] — pre-listener restore: staged pinned bytes, quarantine validation, sanitized re-snapshots as the sole target input, trust continuity (same space + signing authority, never fresh auth), exact post-restore stream inventory, and a journaled liveness claim covering the whole pre-commit window so concurrent commands refuse rather than roll back a live attempt.
  • cotal clean restore-attempt|restore-fallback --attempt <id> --force — the only (explicit) recovery/cleanup verbs.

Review

Adversarial mesh panel (engineering + security lanes) over three rounds; increments 1–2 double-APPROVED, including fixes for a critical concurrent-rollback race, a preserve-state wedge, quarantine sanitization, claim-first backup ownership, atomic completion markers, fail-closed cleanup, listener re-entry, and wire-truth quiescence. Final full-feature verdict pending the last test suite (see below).

Tests

Unit: workspace maintenance state machine (198 checks), manager preservation/resume, auth continuity, core backup, CLI. Live gates: smoke:backup-perms:live (permission deny matrix), smoke:backup-restore:live (15 scenarios incl. six crash-boundary re-entries, claim races, backup→restore→backup→restore, source-immutability), smoke:backup-faults:live (per-stage fault injection + exact-ID timeout whole-attempt replay), smoke:backup-conservation:live (cursor stillness + post-restore delivery). These tests surfaced and fixed two real restore-breaking bugs (born ACK floors on by_start_sequence and truncated-WorkQueue DeliverAll durables). Outstanding: smoke:backup-usermode:live (user-auth lifecycle) is in flight and lands on this branch before merge.

No-consumer native snapshots with ordered chunk sinks, restore
initiation/upload/finalize with exact-ID sessions, canonical stream
config and state validation, conservative pull-consumer checkpoints,
exact per-phase backup/restore/infrastructure permission profiles, and
space-auth trust validation. The deployer profile gains point-reads on
the manager and delivery lease buckets for the preservation quiescence
proof.
One fsynced, generation-bound journal drives the preserve/backup/restore
state machine: cut-intent -> cut-committed -> ready, backup claims with
coordinator/owner liveness and attempt-owned path slots (pending before
creation, inode-proven after), restore-ready records carrying a liveness
claim for the whole pre-commit window, listener proofs with dead-listener
replacement for both restore and ordinary resume, and fail-closed
recovery that removes only provable attempt residue and never surrenders
ownership over unproven cleanup.
Two-phase preserve (prepare fences lifecycle and builds the resume
inventory, commit stops children with every leave/deprovision
suppressed), inventory validated against the exact resume control
schema before any stop, durable commit tokens with idempotent
commit/finalize recovery, resume that adopts retained principals and
never provisions replacements, and abort that reconciles exits
suppressed under the fence. Runtime providers (pty/tmux/cmux/orca)
report authoritative child status so stops are provable.
Non-secret authority fingerprints bind full artifacts to the exact
account identity, active signing set, and (in user mode) owner-secret,
IdP, issuer, and actor-ledger inputs; drift fails closed before any
store mutation. Restore never creates fresh auth.
cotal down --preserve-state fences Plane 3 before the inventory, proves
control-plane quiescence on the wire (leases, not pidfiles), and
journals the manager commitment before any stop so retries never need a
live manager. cotal backup claims ownership before creating clone,
broker run files, or destination, snapshots through a watchdog-owned
isolated broker, and publishes the manifest atomically as the completion
marker. cotal up --restore quarantines, re-snapshots sanitized bytes as
the sole target input, asserts the exact post-restore inventory, and
binds a provable normal listener; ordinary resume adopts or replaces its
bound listener and opens only the exact preserved store inode. Explicit
recovery lands as cotal clean restore-attempt. Replaces the interim
channels export surface.
Operator sequence, artifact and checkpoint semantics, trust continuity,
restore liveness claims and recovery recourse, and the maintenance
cleanup verbs; regenerated docs bundle.
Env-gated failure points at snapshot initiation, first chunk, close, and
manifest publication, a synthetic exact-ID handoff timeout per stream,
and owned placeholder live smokes for conservation, user-mode restore,
and fault/timeout replay coverage.
A fresh by_start_sequence durable is born with a native stream floor of
opt_start_seq - 1 (verified on nats-server 2.14), so the zero-floor
assertion made any space that had ever ACKed a chat message unrestorable.
Validate the exact born floor and a zero delivered count instead - the
conservative at-least-once replay semantics are unchanged. The restore
fault hook also gains a pass discriminator (quarantine:/target:) so the
partial-target-install path is reachable in tests.
Dev-only dependencies so the plan-required live smokes in bin/smoke can
read broker state (pre-cut consumer floors, post-restore counts)
directly; nothing ships in the published package.
All four artifact stages (initiation, first chunk, close, manifest)
fail closed to ready with an absent destination and a clean retry on the
same path; the exact-ID handoff timeout on the final stream - injected
on the quarantine rehearsal AND the real-target pass - discards the
whole populated attempt, returns the identical source inode, and the
clean replay restores with no duplicate messages (counts proven through
a verification re-cut and re-snapshot).
…Queues

The unified born-floor rule is effectiveStart - 1 with effectiveStart =
max(opt_start_seq ?? 1, stream first_seq): a TASK role durable that
completed any work leaves a truncated WorkQueue whose fresh DeliverAll
consumer is born at first_seq - 1, which the previous assertion refused
as a non-born floor. At-least-once replay semantics are unchanged; the
acked item is gone from the stream and everything remaining replays.
Live pre-cut consumer floors sampled through the backup inspect cred
after daemon drain must equal every artifact checkpoint exactly; the
artifact carries only the five canonical durables; all eight streams
conserve messages and sequence bounds; unacked pre-cut work replays on
ordinary resume while acked work stays gone. After a full restore, the
member's pending entry is delivered under its preserved authorization
and a first NEW post reaches it durably through the rebuilt
fanout/inbox/ACL/DLV path. Anti-vacuous guards refuse an all-zero-floor
mesh so the blind spot that hid two restore bugs cannot return.
try {
return await task;
} finally {
if (this.resumeCommitTask === task) this.resumeCommitTask = undefined;
let brokerOwner: ProcessOwner | undefined;
try {
const handshakeDeadline = Date.now() + BOOT_TIMEOUT_MS;
while (!brokerOwner) {
Comment on lines +2 to +20
import {
chmodSync,
closeSync,
constants,
fstatSync,
fsyncSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
realpathSync,
readSync,
readdirSync,
renameSync,
rmSync,
statSync,
writeFileSync,
writeSync,
} from "node:fs";
writeFileSync,
} from "node:fs";
import { hostname } from "node:os";
import { dirname, join, resolve } from "node:path";
let sanitized: { path: string; identity: { dev: bigint; ino: bigint } } | undefined;
let broker: IsolatedBroker | undefined;
let quarantineBroker: IsolatedBroker | undefined;
const recordResources = (input: { owners?: readonly ProcessOwner[]; ownedPaths?: readonly AttemptOwnedPath[] }) => {
Folds the manifest-extension manager resolution, detached-summary
rendering, and five new smoke:ci suites in with the backup/restore
maintenance surface (union of manager options, detach return shape,
and the check/smoke:ci gate chains); docs bundle regenerated.
else { fail++; console.log(` ✗ FAIL: ${name}`, extra ?? ""); }
};
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
const until = async (cond: () => boolean | Promise<boolean>, ms = 10_000): Promise<boolean> => {
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