DreamLake

Living dev note. Iterate freely.

Goal: skip the R2 release pipeline when iterating on nymph against multiple remote daemons. Today the inner loop is:

edit → cargo zigbuild → rsync to daemon-1 → restart → smoke
                     ↘ rsync to daemon-2 → restart → smoke   (manual fan-out)

With nymph push, it becomes:

edit → cargo zigbuild → lakeshore nymph push ./nymph → daemons OTA themselves

Trades a CDN/R2 step for a controlplane endpoint. Fine for debug fleets; not a replacement for tagged R2 releases.

Surface

lakeshore nymph push <file> [options]

Arguments:
  <file>                 path to a local nymph binary

Options:
  --name <label>         human-readable label (default: derived from filename)
  --target <triple>      target triple (default: detected from ELF — e.g.
                         x86_64-unknown-linux-gnu)
  --update <daemon...>   after upload, send a `daemon update` to each
                         (accepts --update --all, --update --prefix <pre>)
  --json                 emit { sha256, url, label } as JSON

Output (human form):

✓ uploaded nymph-x86_64-unknown-linux-gnu (4.0 MB)
  sha256: d99eaffe…b1eeb2e
  label:  debug-2026-05-22T16:42Z
  url:    /v1/nymph-releases/by-sha/d99eaffe…b1eeb2e/nymph

Server endpoints

Two new admin-authed routes in lakeshore-controlplane:

POST /v1/admin/nymph-releases       (multipart: file, label, target)
  → 201 { sha256, label, target, url, size, createdAt }

GET  /v1/nymph-releases/list        (admin — list all)
  → [ { sha256, label, target, size, createdAt }, ... ]

GET  /v1/nymph-releases/by-sha/:sha/nymph   (un-authed — daemons pull from here)
  → application/octet-stream

The download path is un-authed (matches /v1/daemon/* patterns) — auth is by knowing the SHA. Daemons OTA via the existing daemon update command, pointing at this URL.

Storage

v1: Mongo blob. Nymph is ~4 MB; Mongo's 16 MB document limit is plenty of headroom. New collection NymphRelease with shape:

ts
model NymphRelease {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  sha256    String   @unique
  label     String
  target    String   // "x86_64-unknown-linux-gnu", etc.
  size      Int
  blob      Bytes
  createdAt DateTime @default(now())
}

Pros: zero new infrastructure, works on Heroku immediately, blob is durable.

Cons: bloats mongo backups; reads pull 4 MB through Prisma per download. For debug-fleet use (a handful of daemons, few releases per day) this is fine. If usage grows, swap to S3 with a signed URL.

CLI plumbing

  • New src/cli/nymph/ module — push.ts, list.ts, index.ts.
  • Re-uses the existing admin auth (LAKESHORE_ADMIN_TOKEN).
  • Detects target triple from ELF magic + machine field. Reject Mach-O (friendly hint to cross-compile).
  • --update flag glues into the existing runDaemonUpdate from src/cli/daemon/update.ts — same --all / --prefix semantics.

Out of scope (v1)

  • Garbage collection / TTL on releases. Hand-prune via nymph delete <sha> for now (add the verb if/when it's annoying).
  • Signing / verification beyond sha256. The download URL embeds the sha; the daemon recomputes and compares.
  • Multi-target uploads in one command. Push each separately.

Tests

  • Server: upload → list → download → verify sha. Concurrent uploads of the same binary collapse to one row (uniqueness on sha256).
  • CLI: file-not-found → exit 2; Mach-O input → friendly error; happy path → POSTs multipart and prints the URL.

Open question

  • Do daemons need to resolve the URL? The existing OTA path takes a URL. If the controlplane URL changes (Heroku ⇄ prod), old daemons holding a controlplane-relative URL get stuck. Maybe the CLI prints a fully-qualified URL based on LAKESHORE_URL. Then daemon update just curls it.