This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Canonical cross-repo reference: the architecture + cross-repo contracts live in consensus-docs → https://docs.consensus.canister.software/protocol/architecture/ (source). Read it before changing the tunnel handshake/frames/messages (src/tunnel/, src/crypto/) or the routing-ticket format — those must stay compatible with consensus (server/features/node-tunnel/). Related repos: consensus (orchestrator), consensus-client (SDK + CLI), consensus-docs (docs), consensus-facilitator (x402 facilitator).
Verifiable Bun worker node runtime for the Consensus network. Written from scratch (the instance/ directory in the wider monorepo is a reference implementation, not used here). Runtime: Bun ≥1.3, TypeScript strict, ESM, moduleResolution: "Bundler". Source is run directly with bun src/<entry>.ts — there is no build step for local development; tsc is used only for type-checking (bun run typecheck).
Each top-level lifecycle phase has its own entry file under src/ and a matching bun run script:
bun run start— runtime server (src/instance.ts), loopback-only by default (NODE_HOST=127.0.0.1, port:9090); serves local operator endpoints (/health,/node/*) plus a now-dormant/connectroute. In production it runs alongside the control tunnel as one unit (src/supervise.ts). The client-facing data plane rides the control tunnel via the orchestrator node-gateway, so the node opens no inbound port and terminates no TLS — seedeploy/README.md.bun run setup— interactive join wizard (recommended path; orchestrates eval → register → verify).bun run eval— encrypted eval over the tunnel; passing eval writesjoin-auth.jsoninto the state dir.bun run register— submit join payload (requiresjoin-auth.jsonfrom a prior eval).bun run control— long-lived encrypted control tunnel with exponential reconnect. This is the node's whole data path: heartbeats, proxy work, and the client-facing data plane, which the orchestrator node-gateway bridges onto its streams ({kind:"data-plane"}→serveDataConnection, viasrc/clients/data-plane-stream.ts). In production it runs together with the runtime server under one supervised unit (src/supervise.ts, which the PM2/systemd configs exec).scripts/run-control.sh(control-only) is kept for reference.bun run verify— server-side check that the registered node key signs the local manifest.bun run update/bun run update -- --download— compare local manifest to server/update/latest; optional verified download.bun run release -- --version X --commit … --platform … --download-url …— produce tarball + admin manifest indist/.bun run version:bump -- patch|minor|major— explicit version bump commit (ordinary commits do NOT publish).bun run typecheck—tsc --noEmit.
Tests are individual Bun scripts, not a unified test runner. Run a single test with its named script:
bun run test:secure-channel
bun run test:handshake
bun run test:eval-client
bun run test:control-client
bun run test:register
bun run test:benchmarks
bun run test:streams
bun run test:update-reply-to
Most subcommands read configuration from env vars; defaults are not inferred from a config file. Common ones:
CONSENSUS_SERVER_URL— base HTTPS URL; the tunnel URL is derived by swapping scheme towssand path to/node/tunnel.CONSENSUS_TUNNEL_URLoverrides explicitly.CONSENSUS_STATE_DIR— defaults to~/.consensus/node. Holdsconfig.json,keys/,release-manifest.json,join-auth.json,setup-progress.json,downloads/.CONSENSUS_NODE_INSTALL_DIR— production install root (~/.consensus/node-runtimeby default). Containsreleases/<version>/and acurrentsymlink.CONSENSUS_NODE_UPDATE_COMMAND— installer command run on apply; falls back toscripts/install-release.shfrom the local repo or from<install-dir>/current/.- Registration extras:
CONSENSUS_NODE_IPV4,CONSENSUS_NODE_IPV6,CONSENSUS_NODE_PORT,CONSENSUS_NODE_CONTACT,CONSENSUS_EMAIL_VERIFICATION_TOKEN,CONSENSUS_EVM_ADDRESS,CONSENSUS_SOLANA_ADDRESS,CONSENSUS_ICP_ADDRESS.
The codebase is organized around lifecycle entrypoints at src/*.ts that compose helpers from the subdirectories below. Read entries in this order to get the big picture: instance.ts → eval.ts → register.ts → control.ts → update.ts.
All non-trivial server interaction goes through a single WebSocket-based tunnel protocol:
tunnel/handshake.ts— versioned JSON handshake (consensus-node-tunnel/ version 1). Init is signed with the node's Ed25519 identity (crypto/identity.ts); both sides do an X25519 exchange and derive ChaCha20-Poly1305 keys via HKDF over a SHA-256 transcript hash (crypto/secure-channel.ts). The transcript is the canonical-JSON serialization (crypto/canonical-json.ts) of the init message without itssignaturefield.tunnel/frames.ts+tunnel/messages.ts— binary frame format and the discriminated-union message types (MESSAGE_TYPE). After handshake, every message is encrypted as one frame and parsed back into aTunnelMessage.tunnel/tunnel-client.ts+tunnel/connect.ts— single client that supports both modes via theTUNNEL_MODE("eval" | "control") parameter on the init message.
When working with tunnel logic, prefer extending MESSAGE_TYPE and the TunnelMessage union over inventing parallel transports. The handshake signing payload deliberately excludes signature and runs through canonicalJson — do not bypass that path.
eval-client.ts and control-client.ts are thin wrappers over connectEncryptedTunnel that own per-mode state machines:
- Eval client opens an eval tunnel, runs benchmark/integrity actions on demand from the server, and writes
join-auth.jsonwhen the server emitsJOIN_READY. Eval consumes the encrypted authorization; it does not require port-forwarding or a public benchmark endpoint. - Control client is the production long-running loop. It sends heartbeats, executes
proxy_requestandstream_*messages, multiplexes a "public tunnel" frame format (5-byte type+stream_id header) across server-driven streams, serves the client-facing data plane when the orchestrator opens a{kind:"data-plane"}stream (src/clients/data-plane-stream.tsadapts the tunnel stream toserveDataConnection), and owns theupdate_prepare→update_ready→update_applyflow. On apply it closes the WS with code1012, then exits with code0so the supervisor restarts via thecurrentsymlink.
src/control.ts wraps startControlClient in an exponential-backoff reconnect loop (capped at ~30 s + jitter); do not move retry logic into the client itself.
src/update.ts defines the manifest comparison (compareManifests checks version, platform, commit, routes_hash, tarball_sha256) and downloadAndVerify (SHA-256 against the manifest before writing into state/downloads/ with mode 0600). control-client.ts reuses both helpers for the over-the-tunnel apply flow. The control client looks for the installer command in this order: CONSENSUS_NODE_UPDATE_COMMAND → ./scripts/install-release.sh (cwd) → <install-dir>/current/scripts/install-release.sh.
Hosted by runtime/server.ts (Fastify + @fastify/websocket) and the same eval actions exposed through the tunnel:
runtime/eval.tsdispatchesEvalActionvalues (capabilities,integrity,benchmark_*) used by both the local HTTP API and the eval tunnel.runtime/benchmarks/— SHA-256 CPU throughput, ChaCha20-Poly1305 throughput, event loop, memory, system info. Add new suites underbenchmarks/suites/and wire them intobenchmarks/index.ts+runtime/eval.ts.runtime/proxy-command.ts(one-shot HTTP proxy) andruntime/proxy-session.ts/proxy-worker.ts(multiplexed proxy streams).runtime/capabilities.ts— declaredNodeCapabilityset, sent in heartbeats and join payload.
node/state.ts owns all on-disk layout under CONSENSUS_STATE_DIR and is the only place that should touch those paths. crypto/identity.ts lazily creates the Ed25519 keypair under keys/ with mode 0600; the same key signs handshakes, manifests (node/manifest.ts), and integrity payloads (node/integrity.ts). Treat the Ed25519 public key as the node's stable identity — registration binds it to a node_id.
src/release.ts builds a tarball, signs a ReleaseManifest (src/types.ts), and emits an /admin/manifest payload that the Consensus server consumes to gate updates. GitHub Actions' Release workflow is manual.
In production:
ecosystem.config.cjsconfigures PM2 to run<install-dir>/current/src/supervise.ts, which runs the control tunnel (bun run control, the data path) and a loopback-only runtime server (bun run start) as one unit and exits if either does, so anupdate_apply(or a crash) restarts both from the refreshedcurrent. The client-facing data plane is bridged onto the control tunnel by the orchestrator node-gateway, so the node opens no inbound port and terminates no TLS. Thesystemd/unit execs the same entry point via its#!/usr/bin/env bunshebang, and the macOS LaunchDaemon runspm2-runtimeagainst this same config.scripts/install-release.shis the default installer: unpacks the verified tarball intoreleases/<version>/, installs prod deps with the lockfile, atomically moves thecurrentsymlink, then prunes old releases perCONSENSUS_NODE_RELEASE_RETENTION(default 3) — while protecting the release that is mid-update.scripts/ensure-pm2.shandscripts/start-pm2.shbootstrap PM2 on macOS (Homebrew → Node → PM2). For boot persistence WITHOUT a login,scripts/install-launchd.sh(macOS, needs sudo) renderslaunchd/com.consensus.node.plist.templateinto/Library/LaunchDaemonsand runspm2-runtimeunder it; on Linux usesystemd/consensus-node.service. Do NOT usepm2 startupon macOS — it emits a LaunchAgent, which loads only at user login. The installer runsbun run secrets:checkas the daemon's account first and refuses to install if the encryption data key is not readable without a login. FileVault must be off on a node: it halts at a pre-boot unlock prompt, so nothing — daemon or agent — runs until a human types the password.scripts/node-service.shoperates the unit (restart/status/start/stop/ping/logs);restartwaits for the orchestrator to report the node active again, which is the only signal that it actually came back rather than merely relaunching.
The wrapper still tolerates the legacy exit code 75 from older releases. New code should exit with 0 (the supervisor handles the restart) and close with WS code 1012 so the server distinguishes update shutdowns from crashes.
- Logs go through
src/log.ts'slog.info/warn/error(scope, event, fields)— one JSON line per call. Avoidconsole.logoutside of CLI-output paths ineval.ts,update.ts,verify.ts,release.ts. - Signed payloads (handshake, manifest, integrity) MUST round-trip through
canonicalJsonbefore signing/verifying. - Sensitive files (
keys/*,join-auth.json, downloaded artifacts,setup-progress.json) are written with mode0600. Preserve that when adding new on-disk state. - Version bumps are explicit commits using
bun run version:bump; do not changepackage.jsonversionby hand as part of unrelated changes.