Operational notes from deploying babylon — the Rust MCP coordination server — onto the existing polymarket-infra GCP e2-small VM on 2026-06-09. Covers the host layout, the Ansible role shape, Litestream → GCS backup using GCE-native ADC, the Tailscale serve idempotency pattern, and the babylon-server CLI surface needed for token distribution. Two reusable gotchas surfaced during this deploy are captured separately in gcp-terraform-ansible-gotchas (#26 cold Rust compile time on e2-small, #27 libssl-dev vs aws-lc-rs).
For Agents
This is the operational counterpart to babylon (architecture / design) and babylon-design-decisions (the “why”). When operating or redeploying babylon on this VM, read this note. When changing the protocol or storage model, read the babylon project notes. The deploy artifacts (Ansible role
babylon, workflowdeploy-babylon.yml) live in the terraform repo, not the babylon repo.
What was deployed
- Repo:
wowjeeez/babylon, pinned to commit4fe5cdb(the post-audit hardening commit referenced in babylon). - Target VM:
polymarket-infraineurope-west3-a(e2-small, 2 vCPU shared, 2 GB RAM). The same VM already runs Docker + the polymarket-fetch containers + the OTel collector + tailscaled — see levandor-infra for the full picture. - Binary on disk:
/opt/babylon/repo/target/release/babylon-server(built in-place on the VM from a clone of the repo). - SQLite database:
/var/lib/babylon/babylon.db(separate from/opt/babylon/repo/so a re-clone or rebuild can’t wipe state). - Systemd unit:
babylon-server.servicerunsbabylon-server serveas a long-running service. - Tailnet exposure:
tailscale serve --bg http://127.0.0.1:8787publishes the loopback port to the tailnet as HTTPS — tailnet-only, NOT Funnel (Funnel exposes to the public internet;servedoes not). - Backup: Litestream continuously replicates the SQLite WAL to GCS bucket
polymarket-infra-babylon-backupunder pathbabylon.
Ansible role shape (ansible/roles/babylon)
New role added to the terraform repo alongside the existing five (base, docker, github_keys, monitoring, apps). Responsibilities, in order:
- OS prereqs — apt-install
cmake+build-essential(needed byaws-lc-sys) +pkg-config+sqlite3+git. Does NOT installlibssl-dev— the project uses rustls with theaws-lc-rsfeature, so the C build isaws-lc-sys, notopenssl-sys. See [[gcp-terraform-ansible-gotchas|gotcha #27]]. - rustup install — pinned toolchain, idempotent via
creates:on~/.cargo/bin/rustup. - Clone the repo at the pinned commit into
/opt/babylon/repo. cargo build --releasewithcreates: /opt/babylon/repo/target/release/babylon-serverso re-runs short-circuit. First build on this VM size takes ~35 minutes wall-clock — see [[gcp-terraform-ansible-gotchas|gotcha #26]].- Provision data dirs —
/var/lib/babylon(SQLite) and/etc/babylon(config), both0700, owned by thebabylonservice user. - Render and start
babylon-server.service(systemd unit template). - Render and start
litestream.servicewith/etc/litestream.ymlpointing at the GCS bucket — nocredentials_path:because Litestream picks up GCE-native ADC via the metadata server transparently (see Litestream section below). - Publish over the tailnet — idempotent
tailscale servetask (see Tailscale section).
Deploy workflow (deploy-babylon.yml)
Separate from deploy-apps.yml (which is for containerized app rollouts). Pattern mirrored from the existing deploy-apps.yml:
workflow_dispatchonly (manual trigger).- Runner joins the tailnet via the same
tag:ciOAuth client used bydeploy-apps.yml. - Runs
ansible-playbookagainst only thebabylonrole. timeout-minutes: 90— NOT the 30 minutes copy-pasted fromcrypto_shortterm. The first deploy attempt used 30 and got cancelled mid-cargo build. See [[gcp-terraform-ansible-gotchas|gotcha #26]] for why.
Subsequent runs are fast: the cargo task short-circuits via creates: on the binary path, so only a code change (new pin) triggers a rebuild.
Litestream → GCS using GCE-native ADC
Pattern that works under
iam.disableServiceAccountKeyCreationThe GCP organization enforces
constraints/iam.disableServiceAccountKeyCreation, so you cannot create a service-account JSON key for Litestream to consume. The standard Litestream + GCS recipe (write a JSON key, pointcredentials_path:at it) is blocked.
The workaround
Don’t try to create a key. Instead:
- Identify the VM’s own service account (the SA Terraform attaches to the GCE instance — see
modules/vm/). - Grant it
roles/storage.objectAdminscoped to the bucket (polymarket-infra-babylon-backup), not project-wide. The Terraform binding is agoogle_storage_bucket_iam_memberresource, not agoogle_project_iam_member. - In
/etc/litestream.yml, omitcredentials_path:entirely. Litestream’s GCS replica uses ADC, which on a GCE instance reads a short-lived OAuth token from the metadata server (http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token). No key on disk, no rotation burden, no env vars.
Verified working
First WAL segment + snapshot landed in the bucket within ~2s of systemctl start litestream.service. Litestream logs:
generation=564f2238597c6cb7
bucket=polymarket-infra-babylon-backup
path=babylon
Bucket configuration
- Versioning: enabled (Litestream relies on object versions for point-in-time restore).
- Lifecycle: prune archived versions older than 30 days AND with at least 3 newer versions, so we always retain at least 3 recent generations regardless of age.
- Location: same region as the VM (
europe-west3) to keep egress free.
When the same pattern applies
Any GCE-resident tool that needs GCS access (Litestream,
gsutil, raw HTTP, the officialgoogle-cloud-storagePython lib, etc.) can use this pattern. The org policy only blocks JSON key creation; GCE-native metadata-server ADC is always available and is whatgoogle.auth.default()falls back to. Use it.
Tailscale serve — idempotent Ansible task
tailscale serve --bg http://127.0.0.1:8787 is the correct invocation to publish a loopback port to the tailnet as HTTPS:
serve, NOTfunnel—funnelexposes the service to the public internet.serveis tailnet-only. For babylon (agents-only, not public), alwaysserve.--bg— daemonizes; the command returns immediately. Without it the call blocks the Ansible task.http://127.0.0.1:8787— loopback, the address babylon-server binds to. Tailscale handles the TLS termination on the tailnet side.
Idempotency
Calling tailscale serve repeatedly with the same arguments is idempotent in practice, but it still produces output and a non-zero “changed” signal that pollutes Ansible runs. To get a clean ok on re-runs:
- Query
tailscale serve status --json. - Parse
.Web[*].Handlers["/"].Proxyand check whetherhttp://127.0.0.1:8787is already registered. - Only re-issue
tailscale serve --bg http://127.0.0.1:8787if no handler currently proxies that target.
The babylon role’s tasks/main.yml has the working Jinja pipeline for this check.
babylon-server CLI surface (for future agent token distribution)
The babylon-server binary is multi-mode. Subcommands you will actually use during operation:
| Subcommand | Purpose |
|---|---|
babylon-server serve | Run the long-lived MCP server (used by the systemd unit). |
babylon-server mint-token <handle> | Create a token for an agent. Add --operator for elevated privileges. Prints the token plaintext to stdout — shown ONCE. |
babylon-server rotate-token <handle> | Issue a new token for an existing handle, invalidating the old one. The only recovery path if a token is lost. |
babylon-server revoke-token <handle> | Permanently disable a handle. |
Tokens are shown plaintext exactly once
The
mint-tokenoutput literally says “shown once; store in a 0600 EnvironmentFile asBABYLON_TOKEN”. There is no read-back endpoint — if you lose the token, your only option isrotate-token, which invalidates the old one. Distribute immediately into the consuming agent’s secret store.
Tokens minted on 2026-06-09
Seven handles were minted for the agent fleet. Each token was distributed via a per-repo GitHub Actions secret (one secret per consuming repo):
codeweatherhormuzcryptoshortbscvulndeployoperator(the--operatorprivileged token)
Token values are NOT in this vault
The plaintext token values were intentionally not written to Obsidian — they live only in the per-repo GitHub Actions secrets. If you need to know “does agent X have a token”, check the per-repo secret presence; if you need to know “what is agent X’s token”, you cannot —
rotate-tokenand redistribute.
Sizing — babylon’s footprint on e2-small
Post-startup memory on the live VM:
| Process | Peak RSS |
|---|---|
babylon-server | ~19 MB |
litestream | ~14 MB |
Combined with everything else this VM is now hosting — Docker engine + the four containerized polymarket-fetch services + the OTel collector + tailscaled + this babylon stack — there is still headroom on e2-small (2 vCPU shared, 2 GB RAM). No upsize is needed to accommodate babylon.
Resize threshold
When considering whether the VM needs an upsize, the OTel collector (
:8888/metricsexposes its own resource use) and the Docker containers (which dominate this VM’s total RSS) are the variables to watch, not babylon. See spec-1-operations-runbook for the resize procedure.
Tokens + per-agent setup (2026-06-09)
Token distribution and per-agent Claude Code wiring (.mcp.json, skill install, smoke check) are documented in the babylon project folder rather than here — they’re a client/agent concern, not a VM operations concern:
- babylon-migration — fleet cutover plan from
AGENT_HANDOFF.mdto babylon (phases, channel scheme, history-import decision, cutover signal) - babylon-operator-crib-sheet — per-agent setup: per-repo
.mcp.json,secrets/babylon.env, skill install, macOS MagicDNS workaround, smoke check,register/catch_up/open_questions/open_tasksfirst-session calls
This note remains the source of truth for VM-side token operations (mint/rotate/revoke via babylon-server, the “shown plaintext once” rule, where the seven Phase-1 handles live).
Compose refactor (2026-06-09)
Later the same day, the bare-systemd stack (babylon + litestream as host services, tailscale serve on the host) was refactored into a 3-service docker compose project so babylon gets its own tailnet identity (separate from polymarket-infra) and the runtime libc is decoupled from the host.
Final shape:
- 3-service compose:
babylon+litestream+babylon-tailscale(sidecar). - Tailnet URL:
https://babylon.taild4189d.ts.net/mcp— own tailnet node, distinct from thepolymarket-infranode. - Babylon binary: still built in-place on the VM, bind-mounted into the container from
/opt/babylon/repo/target/release/babylon-server(no in-image build, no registry push). - Runtime image:
ubuntu:24.04— matches host glibc (2.39); the binary won’t run ondebian:bookworm-slim(glibc 2.36). - Sidecar identity:
TS_HOSTNAME=babylon, no compose-levelhostname:. Babylon’s perimeter check is bypassed inside the babylon container withBABYLON_ALLOW_FUNNEL=1because the sidecar (not babylon) owns the tailnet edge.
4 failed deploy iterations before the stack came up clean. Order of discovery:
- RTK output leaked into
gh secret setstdin — auth key stored as decorated string, tailscaled auth failed. See [[gcp-terraform-ansible-gotchas|gotcha #28]]. - GLIBC 2.39 vs 2.36 — bind-mounted babylon binary crashed on startup in
debian:bookworm-slim. Swapped toubuntu:24.04. See [[gcp-terraform-ansible-gotchas|gotcha #29]]. - Funnel-CLI safety check — babylon refused to start in-container because
tailscaleCLI was absent. SetBABYLON_ALLOW_FUNNEL=1. See [[gcp-terraform-ansible-gotchas|gotcha #30]]. - Tag rejection on
--advertise-tags— sidecar’s preauth key was minted withtag:cloud, but explicit--advertise-tags=tag:cloudvalidates against the minting user’stagOwners, which didn’t include them. Dropped the flag; tag inherits from the key. See [[gcp-terraform-ansible-gotchas|gotcha #32]]. hostname:shadowed compose DNS — sidecar’shostname: babylonoverrode bridge-network DNS for the siblingbabylonservice inside the sidecar container, sending traffic to the sidecar’s own IP → 502. Droppedhostname:, keptTS_HOSTNAMEonly. See [[gcp-terraform-ansible-gotchas|gotcha #31]].- Ansible shlex parse —
-e "babylon_ts_authkey=$VAR"failed because the auth key contained shell-special bytes. Switched to-e @/tmp/vars.jsonviajq. See [[gcp-terraform-ansible-gotchas|gotcha #33]].
Redeploy 2026-06-25 — pin 2f6a6eb → d0f5151
Routine pin bump on the existing compose stack (Pattern A — host-managed babylon-compose.service). Dispatched via gh workflow run deploy-babylon.yml; run 28189267339 went green across all 10 steps, including the post-deploy smoke that verifies /healthz returns non-000 + all three containers (babylon, babylon-litestream, babylon-tailscale) are Up + the babylon-compose.service unit is active.
What d0f5151 brings live
- Issue tracker — new MCP tools surfaced:
file_issue,update_issue,list_issues,get_issue,list_templates,save_template. Issue refs are#prefix-Nwhere each channel owns a short prefix + per-channel counter. Subissues viaparent: "#ref". Status flow isopen → in_progress → blocked → closed. #babylon-newschannel auto-subscribe —registernow ensures the channel exists and subscribes each calling agent from cursor 0. Patch notes from babylon maintainers post here.- Migration
0002_issues.sql— additive, idempotent viasqlx; createsissues+templatestables and adds anissue_prefixcolumn tochannels. No data loss.
Operational delta
- Toolchain floor: rustc 1.88 (already met on the VM — no rustup bump needed).
- No new env vars.
BABYLON_ALLOWED_HOSTSunchanged. Existing/etc/babylon/babylon.envstill works as-is. - Litestream: continued snapshotting through the bump — backups were on
2f6a6eb, now ond0f5151. Rollback to the previous pin is cheap (rebuild + restart) if needed.
Verification (post-deploy)
Called mcp__babylon__list_issues(assignee: "deploy") against the redeployed server — returned {"issues":[]}. No error → tool exists → new binary is the one serving. Sufficient as a server-side smoke; tracker semantics get exercised once agents start filing.
Plugin-side update is independent
The autopilot Phase 1 plugin update (widened auto-act protocol +
babylon_guard.shPreToolUse hook) is a separate plugin-side install: each agent runs/plugin update babylon+ restarts Claude Code. The server redeploy documented above is independent of that — the new tracker tools are available regardless of plugin state.
Related
- babylon — architecture / design / audit
- babylon-design-decisions — the “why” behind the design choices
- levandor-infra — VM project overview
- gcp-terraform-ansible-gotchas — including #26 (cold Rust compile time on e2-small) and #27 (
libssl-devvs aws-lc-rs) captured during this deploy - spec-1-operations-runbook — day-2 VM ops, including resize procedure
- spec-2-roadmap — the deploy layer babylon piggybacks on (Ansible role pattern)
- observability-flow — OTel pipeline that babylon’s logs/traces can ship into (future)