Skip to content

fix: recover an unlinked live daemon registry - #128

Merged
myobie merged 7 commits into
mainfrom
agent/live-daemon-recovery
Jul 31, 2026
Merged

fix: recover an unlinked live daemon registry#128
myobie merged 7 commits into
mainfrom
agent/live-daemon-recovery

Conversation

@myobie

@myobie myobie commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

A live daemon and PTY child survive when an external cleanup unlinks their pathname socket, pid sidecar, and metadata. Existing clients continue over accepted sockets, but new clients cannot find the daemon. Relaunching the stored command would create a duplicate provider.

Closes #126.

Change

  • Supporting daemons advertise a versioned recovery capability only when the selected PTY_ROOT and its .recovery directory are owned, private, non-symlink directories with bound inode identities.
  • pty recover <name> --snapshot <metadata.json> validates the exact root, recovery directory, daemon PID, generation, process-start token, launch identity, and capability before publishing an HMAC-authenticated request.
  • A retained signed metadata revision changes with metadata writes, so snapshots captured before later tags, display-name, attach-state, or lifecycle mutations are refused instead of rolling state backward.
  • Capability-bearing metadata mutations publish the signed revision before renaming new metadata into place. Recovery secret rotation likewise signs the rotated revision before PID or metadata publication; any interruption can disable recovery but cannot re-authorize the old snapshot.
  • The recovery lock is keyed to the already revalidated live daemon/root identity, blocks normal creation under the live daemon PID, performs no liveness signal, and can be resumed after the recoverer dies.
  • The original daemon revalidates privacy immediately before request handling and publication, creates a second listener, publishes pid/metadata with no-replace semantics, rotates the request secret, and retains the unreachable old listener until shutdown so Node cannot unlink the new pathname.
  • Existing client sockets and the PTY child remain untouched. No signal, restart, relaunch, provider duplication, or foreign-path unlink fallback exists.

Fail-closed behavior

Recovery refuses legacy/unsupported snapshots, stale metadata revisions, non-private/replaced/symlink roots or recovery directories, malformed/oversize/symlink requests, wrong secret/PID/generation/start token/launch identity, replayed snapshots, foreign locks, reappeared sidecars, and foreign listener pathnames. Existing pre-protocol daemons never advertise the capability and are not claimed recoverable.

Review-RED closure

  1. Capture, then mutate tags/display name/attach state, then unlink: the old snapshot is refused; the current snapshot recovers without rollback.
  2. Kill the recoverer after its authenticated lock is published: the provider and attached client continue; a later valid recovery resumes and succeeds with one provider launch.
  3. Downgrade the same root or .recovery inode to group/world-accessible after capture: recovery refuses before writing a request, result, lock, socket, pid, or metadata.
  4. At the deterministic mutation seam, old metadata is still visible while the newly signed revision is already authoritative; initial creation and recovered-secret rotation use the same revision-first rule.
  5. Throw at that exact seam before metadata rename: old metadata remains visible, but two consecutive retained-snapshot recovery attempts are refused, publish no socket/PID/metadata, and leave the advanced signed revision unchanged.

Verification

Current exact head f70f85baa89145136a63cc9909fc2423bd3d1e7d (test-only successor to the reviewed implementation):

  • npm run typecheck
  • npm run build
  • Interrupted-publication negative regression — passed five consecutive isolated runs
  • Recovery plus adjacent metadata/lifecycle matrix — 104/104 (recovery 11/11)
  • Local nix build .#pty — passed
  • Hosted Nix 30565910199 — passed
  • Hosted Test 30565910196 — passed

Earlier full-suite flake evidence at predecessor head 2b3d08d600f279e0cf525891808fd6e6183eb552 is superseded by the green exact-head Test above. The production code is identical between those heads; the successor adds only the deterministic interruption regression.

No production recovery was attempted.

Protocol selection

This is the selected issue #126 implementation. PR #127 used an incompatible unauthenticated root-level request protocol without mutable-metadata revision or private-root authorization binding and is closed as superseded.

@myobie
myobie merged commit 9eb958c into main Jul 31, 2026
2 checks passed
@myobie
myobie deleted the agent/live-daemon-recovery branch July 31, 2026 20:31
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.

Recover a live daemon whose pathname socket and registry were unlinked

1 participant