Remote-sandbox support for the credential proxy (+ E2B and Fly.io guides) - #931
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
varlock-website | c2bb758 | Commit Preview URL Branch Preview URL |
Jul 30 2026, 11:35 PM |
Contributor
|
The changes in this PR will be included in the next version bump.
|
commit: |
Bind the proxy off-loopback with `proxy start/run --listen` and serve a built-in CONNECT-over-WebSocket tunnel on the same port, so a client behind provider HTTP ingress (E2B, Modal, ...) can route through it. A non-loopback bind mints a per-session data-plane token (pinnable via VARLOCK_PROXY_TOKEN) that off-loopback peers must present; loopback peers stay exempt and the control endpoint stays loopback-only. `proxy run --url <wss> --token <tok> -- <cmd>` runs a command through a broker running elsewhere: it opens the tunnel, self-wires the child-view env and CA certs from the broker's bootstrap, and runs through a loopback proxy, so the client only holds placeholders. --url/--token are the remote analog of --session and share run's spawn/redaction/teardown core. `proxy env --full` emits the whole child-view env (placeholders + non-secret values), with --proxy-url/--cert-dir to repoint it for a remote client.
The broker runs `proxy start --listen`; agents run `proxy run --url` instead of downloading chisel and hand-wiring env + certs. Adds an install-method note (E2B base ships Node 20, below varlock's floor, so use the binary), and documents --listen / --url / --token / env --full in the CLI reference.
Make enabling remote access a deliberate, self-documenting action rather than a side effect of a bind-address flag. `proxy start --tunnel` (bare = bind 0.0.0.0; `--tunnel=<addr>` picks an interface) serves the WebSocket tunnel and mints the data-plane token. The token remains the security boundary; this is a naming change, not a behavior change.
The docs reorg added an Ecosystem > Sandboxes group; the E2B guide now lives there (with Cloud sandboxes / Local tools sub-groups) instead of being an unlisted page under /guides. Links updated for the split proxy guide pages.
Machines: broker machine on a custom private network reached over 6PN (plain ws://, no public ingress), and the proxy-on-your-machine shape over bidirectional WireGuard peering. Sprites: broker sprite over the sprites.app URL with egress lockdown, plus a Connectors/tokenizer comparison.
The ESM dist bundles CJS deps (ws), whose require('<node builtin>')
calls go through esbuild's interop shim, which needs an ambient require
and threw "Dynamic require of 'events' is not supported" under Node
before any command ran. The shim lives in bin/cli.js (dynamic import to
defeat hoisting) rather than a build-wide banner, so the package keeps
zero runtime dependencies and the runtime/edge chunks stay free of
node:module imports. Verified via packed tarball under Node 24/25 and
the compiled binary.
Broker-sprite recipe rewritten with validated commands: npm install (sprites ship Node 24) + PATH symlink, sprite-env service with --http-port wake routing, public URL requirement (proxy run --url cannot present the sprites token), live egress clamp incl. IP-literal blocking, and sleep/wake behavior notes.
Sprites is where Fly's agent users are, and the sprite recipes are the validated ones. The Machines material compresses into a trailing notes section (env/secrets facts plus a sketch of the 6PN broker and WireGuard dial-back topologies, marked as not yet validated) since a Machines broker cannot serve sprite fleets anyway (sprites are not on 6PN).
…erver The tunnel no longer ships a WebSocket library. The guest side uses the runtime's native WebSocket (global in Node 22+ and Bun); since it cannot set request headers, the data-plane token now rides a Sec-WebSocket-Protocol entry (base64url-encoded), with the x-varlock-tunnel-token header still accepted. The broker side is a minimal RFC 6455 server codec (no extensions negotiated) attached to the existing upgrade handler; under Bun, whose node:http drops manual writes on upgrade sockets, it delegates to Bun's built-in native ws module (resolved at runtime, no package involved). This also removes the bin/cli.js require shim added for the bundled-CJS interop crash, since there is no CJS ws to bundle anymore. Verified: tunnel round trips across all four Node/Bun broker-client combinations, the packed tarball under Node, the compiled binary, and sprite-to-sprite through real sprites.app ingress (subprotocol auth traverses their proxy).
Contributor
📦 Bundle size
dist/ only; native binaries are versioned separately and not counted here. |
The broker recipes wrote a .env.local of real values onto the broker's disk, which assumed the orchestrator had one to give. Schema keys resolve from ambient process env, so the recipes now pass values (or a plugin's secret-zero token) through the platform's own env machinery (E2B envs, sprite service --env) and upload only the schema. The orchestrator runs under varlock run, so values come from wherever its schema says.
The JSON env map takes any value (the CLI --env form is comma-separated and noted as the in-sprite alternative). Validated live, including values with commas, and the heredoc form with shell expansion.
With plugin-resolved schemas (the common case) the broker needs one env var besides the proxy token: the plugin's service-account token. Enumerating values is the fallback for locally-held ones.
The overview now outlines the five topologies (inject values, proxy in-sandbox (not recommended), broker instance, proxy on your infra, proxy on your machine). Each provider guide leads with the recommended broker-instance recipe and compresses the rest to short pointers.
Permissive egress as the shown default (agents only hold placeholders), secret-zero as the shown bootstrap, varlock-broker naming, PROXY_TOKEN generation and explanation in the recipe, nvm PATH symlink explained, multi-env-file note, and a note that the sprite CLI steps map to REST.
Connectors / network.rules move into :::note callouts framed around what varlock adds: transparent substitution, secrets from any source in your custody, and one agent-legible schema.
A restart normally mints a new CA, so clients that already trust the old one fail TLS on their next request. That is fine locally (short sessions, and the CA key deliberately never touches disk) but wrong for a broker, where a restart or a sandbox waking from hibernation silently breaks every running agent. --persist-ca keeps the CA (cert + key, 0600) in --cert-dir and reuses it on the next start, rotating a day before its 30-day expiry. The key on disk is only reasonable where the proxy runs alone, which is exactly the broker case: that machine already holds the real secrets. Default behaviour is unchanged. Verified against a real restart: a CA file captured before the restart still validates certs minted after it (curl exit 0), where the same test without the flag fails with exit 60.
A 30-day life just deferred the breakage the flag exists to prevent: the rotation would drop every agent still running when it came around, and it bought nothing, since nothing checks revocation for this CA and only clients that fetched it from that broker trust it. 10 years matches the mkcert/Caddy local-root convention and stays inside UTCTime's range (RFC 5280 switches to GeneralizedTime past 2049), rather than encoding a literal no-expiry date down a rarer path through TLS verifiers. To retire one, delete the cert directory and restart.
The orchestrator is a Node program, so importing auto-load beats wrapping it in `varlock run`; values then come off ENV. Also brings the guide to parity with the Fly review pass (data-plane token explained inline, ENV-based secret-zero).
`varlock run -- sprite exec --env KEY="$KEY"` expands in the caller's shell before varlock resolves anything, so the sprite got an empty value. Wrap in sh -c so expansion happens inside varlock's env, and show the auto-load orchestrator form as the alternative.
`--inject blob` gives one variable to pass however many secrets are involved; importing varlock/env in the sandbox unpacks it with no .env files, no CLI, and no second resolve. Notes --filter scoping, the varlock dependency, and patchGlobalConsole for redaction, plus a caution covering the shell-expansion traps.
Forwarding __VARLOCK_ENV needs too much manual wiring to recommend: varlock/env rather than auto-load (auto-load always shells out to the CLI, which a sandbox does not have), a separate patchGlobalConsole call for redaction, and --filter to scope it. It is also provider-agnostic, so it was being duplicated per guide. Guides go back to passing the values a sandbox needs; revisit once auto-load can initialize from an injected blob.
…andling --tunnel read as an outbound tunnel (ngrok, cloudflared, fly proxy all use the word that way), which is the opposite of what it does, and the guides had to say "start --tunnel" and "expose it through a tunnel service" on the same page. --expose names the outcome and covers both of the flag's jobs: binding off-loopback and serving the WS tunnel. The data-plane token is a credential, so: - `varlock proxy token` prints it for a session (its own verb, never in status/env output), so a minted token can be read back instead of scraped out of a broker's log. - The startup banner prints it only when stdout is a TTY; headless it points at `proxy token` instead, keeping it out of service logs. - Docs pass it via VARLOCK_PROXY_TOKEN rather than --token, so it stays out of process listings, shell history, and provider command logs.
The snippet used an undeclared `sprite` and awaited `spawn`, which is
the event-based API and returns a SpriteCommand rather than a promise.
Shows the client construction and the promise-based `exec(cmd, { env })`
instead, verified against a real sprite.
It still opened with "serve the built-in WebSocket tunnel", which is the mechanism rather than the purpose and no longer matches the flag name or the reference docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Lets the credential proxy be reached from another machine or a remote sandbox, with no external tunnel tool and no WebSocket dependency, and documents it with E2B and Fly.io guides in a new Sandboxes docs section.
Proxy changes (
packages/varlock)proxy start/run --expose(bare = bind0.0.0.0;--expose=<addr>picks an interface, e.g.::for IPv6): make the proxy reachable from another machine. It binds off-loopback and serves a built-in CONNECT-over-WebSocket tunnel, so a client behind provider HTTP ingress (E2B, Fly Sprites, Modal, ...) can route through it. Mints a per-session data-plane token that off-loopback peers must present; loopback peers stay exempt and the control endpoint stays loopback-only.--exposerather than--tunnelbecause in this ecosystem (ngrok, cloudflared,fly proxy) "tunnel" means an outbound tunnel, the opposite of what this does; the guides otherwise had to say "start--tunnel" and "expose it through a tunnel service" on one page.proxy run --url <wss> -- <cmd>: run a command through a broker running elsewhere. Opens the tunnel, self-wires the child-view env and CA certs from the broker's bootstrap, and runs through a loopback proxy, so the client only ever holds placeholders.--urlis the remote analog of--sessionand sharesrun's spawn/redaction/teardown core. The token comes fromVARLOCK_PROXY_TOKEN(preferred) or--token.proxy token: print a session's data-plane token. The token is a credential, so it gets its own verb (never instatus/envoutput), the startup banner withholds it unless stdout is a TTY (it would otherwise sit in a headless broker's log file), and the guides pass it viaVARLOCK_PROXY_TOKENrather than--tokento keep it out of process listings and provider command logs.proxy start --persist-ca(with--cert-dir): keep the CA (cert + key, mode 0600) and reuse it on the next start, so a broker restart — including a sandbox waking from hibernation — doesn't invalidate agents that already trust it. Off by default, where the CA stays in memory as before. Persisted CAs last 10 years: any expiry would just relocate the breakage the flag exists to prevent, and buys nothing when nothing checks revocation for this CA.proxy env --full: emit the whole child-view env (placeholders for secrets, real values for non-secrets), with--proxy-url/--cert-dirto repoint it for a remote client.WebSocket(Node 22+/Bun); since it can't set request headers, the token rides aSec-WebSocket-Protocolentry (thex-varlock-tunnel-tokenheader still works for other clients). The server is a small built-in RFC 6455 codec on the existing upgrade handler; under Bun (the compiled binary), whosenode:httpsilently drops manual writes on upgrade sockets, it delegates to Bun's built-in nativewsmodule. The bootstrap uses receiver-driven close, since Bun's ws close discards unflushed frames on a real WAN.Docs
/sandboxes/e2b/): broker sandbox, egress lockdown, trust model, and a comparison with E2B'snetwork.rules./sandboxes/flyio/): broker sprite as a service (wake routing viahttp_port, JSON env map), egress lockdown, a Sprites Connectors comparison, and notes for raw Fly Machines..env.local.--expose,--url,token,--persist-ca, andenv --full.Validation
--expose --persist-ca, CA persisted to the sprite's filesystem, token withheld from the service log, and an agent connecting with the token as an env var.--persist-caagainst a real restart: a CA file captured beforehand still validates certs minted after it (curl exit 0), where the same test without the flag fails with exit 60.@fly/spritesAPI and run against a live sprite.