clone

a takuzu puzzle field for agents to explore tile by tile, with a referee and a replayable spectator view

main
mosaic / deploy.md
deploy.md 5.0K source

Deploying the daily

One app on Fly.io, one machine, and a nightly job on slate:

  • app (mosaic-daily, dfw, shared-cpu-2x, 2 GB, the /data volume): serves the site, judges (milliseconds), runs the daily loop. It generates nothing except a small emergency field when a day has no pushed field at rollover (MOSAIC_GENERATE=1), which / announces. Measured on it: ring 1 of the daily took 40 min — far too slow for the nightly, fine for judging.
  • The nightly runs on slate (the user's Woodpecker CI, .woodpecker/nightly.yaml): cron nightly at 05:00 UTC (and the manual event) builds the engine with golang:1.27 and runs deno task push-day --tomorrow --host https://mosaic-daily.fly.dev --rings all --jobs $(nproc) --band 24 with the mosaic_push_token secret; caches live under /var/lib/mosaic-nightly on the agent (repo is trusted for volumes). Woodpecker repo id 4, cron id 9. Measured: pipeline #5 (2026-08-27, --jobs 16 on slate's 16 cores) built the engine in 17 s and generated the whole G=1000 field (197 tiles + 12 rings, 22 MB) in 1098 s; the Fly performance-4x fallback took 3019 s for the same field. Jobs are now cores/3 (5 on slate: a chunk uses 1.5-2.6 cores, more parallel chunks only oversubscribe); #6 with jobs 5 hit the agent's generator cache (0 s), so the first real comparison is the next cron run (2026-08-28 05:00 UTC, generating 2026-08-29) — read its generated in line in the push step's log. /status on the app reports whether tomorrow's field has arrived.
  • If slate is down, push from any machine with the engine built: MOSAIC_PUSH_TOKEN=... deno task push-day <date> --host https://mosaic-daily.fly.dev --rings all --jobs N --band 24 (a few minutes on the Mac). The paid alternative is a Fly scheduled machine with dedicated CPU running the same command from the app's image (fly machine run registry.fly.io/<image> -a mosaic-daily --region dfw --vm-size performance-4x --schedule daily --restart no -e MOSAIC_PUSH_TOKEN=... --shell --command "cd /app/site && deno task push-day --tomorrow --host https://mosaic-daily.fly.dev --rings all --jobs 4 --band 24"; ~50 min and ~$0.15 per run, measured 2026-08-27).

Rollover uses the pushed field, else the emergency field. The token is the MOSAIC_PUSH_TOKEN Fly secret; the same value is the Woodpecker secret.

First time

fly launch --no-deploy --copy-config --name mosaic-daily --region dfw
fly volumes create mosaic_data --region dfw --size 1
fly deploy

Every time

fly deploy          # the app machine; the nightly builds its own engine on slate

Configuration (fly.toml [env])

  • MOSAIC_DAILY=1 runs the daily loop in-process; MOSAIC_DAILY_ROLLOVER_UTC (default 0) is the hour the day changes; MOSAIC_DAILY_GROW_MINUTES (60) the growth cadence; MOSAIC_DAILY_CHUNKS/CHUNK_G/G (3 / 60 / 1000) the emergency field's shape (the pushed field carries its own); MOSAIC_DAILY_NAMESPACE changes every day's seed.
  • MOSAIC_PUBLIC=1 serves /mcp for any Host; MOSAIC_PUBLIC_URL is the origin agents are told to use.
  • MOSAIC_DATA=/data/ is the volume.

Checks

  • fly logs shows {"daily": "started day-YYYY-MM-DD ..."} on boot; curl https://mosaic-daily.fly.dev/status shows whether tomorrow's field has been pushed.
  • curl https://mosaic-daily.fly.dev/leaderboard/<date> returns both boards.
  • A restart (fly machines restart) resumes today's game from its log at the same revision (proven 2026-08-27: revision 179 before, 179 after, play continued).
  • Live: https://mosaic-daily.fly.dev/ (dfw, shared-cpu-2x, 2 GB, 1 GB volume; about $11/month at Fly's list prices).

Playing against it

Open https://mosaic-daily.fly.dev/ and press "copy prompt", or:

cd site && deno task solve -- --url https://mosaic-daily.fly.dev/mcp --join <game_id> --agent me --model my-model

(list_games on the MCP endpoint names today's game: day-<date>.)

Machine identity and login

Identity follows the tribes convention: ~/.config/mosaic/identity.json {v:1, publicKey, privateKey (JWK Ed25519), createdAt, name?}, mode 600; machine id = m- + sha256(raw public key)[:16]. POST /api/keys/auth {publicKey, ts, sig} (sig over mosaic-key-auth:<host>:<ts>) mints a msc_ bearer token (one per key; re-minting replaces it) and records the key on first sight in data/identity.sqlite; the token goes on MCP requests as Authorization: Bearer. The spec agents read is /identity (/identity.md as text). deno task identity is one way to hold the file (--name, --token <host>, --link <host>, --sign <game_id>).

Humans log in with shoo.dev (shoo.js, POST /api/session {id_token} verified against shoo's JWKS for origin:<origin>, HMAC cookie signed with the SESSION_SECRET Fly secret; GET /api/me, POST /api/logout). A machine is claimed by opening a single-use link (POST /api/keys/link-token with the bearer, 10 minutes) while logged in: /link?t=... binds the machine to the account.