Skip to content

Remote-sandbox support for the credential proxy (+ E2B and Fly.io guides) - #931

Merged
theoephraim merged 28 commits into
mainfrom
e2b-guide
Jul 30, 2026
Merged

Remote-sandbox support for the credential proxy (+ E2B and Fly.io guides)#931
theoephraim merged 28 commits into
mainfrom
e2b-guide

Conversation

@theoephraim

@theoephraim theoephraim commented Jul 23, 2026

Copy link
Copy Markdown
Member

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 = bind 0.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.
    • Named --expose rather than --tunnel because 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. --url is the remote analog of --session and shares run's spawn/redaction/teardown core. The token comes from VARLOCK_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 in status/env output), 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 via VARLOCK_PROXY_TOKEN rather than --token to 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-dir to repoint it for a remote client.
  • No WebSocket library is shipped. The client is the runtime's native WebSocket (Node 22+/Bun); since it can't set request headers, the token rides a Sec-WebSocket-Protocol entry (the x-varlock-tunnel-token header 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), whose node:http silently drops manual writes on upgrade sockets, it delegates to Bun's built-in native ws module. The bootstrap uses receiver-driven close, since Bun's ws close discards unflushed frames on a real WAN.

Docs

  • New Sandboxes section. The overview outlines the topologies (inject values, proxy in-sandbox (not recommended), broker instance, proxy on your own infra, proxy on your machine); each provider guide leads with the recommended broker-instance path and keeps the rest short.
  • E2B (/sandboxes/e2b/): broker sandbox, egress lockdown, trust model, and a comparison with E2B's network.rules.
  • Fly.io (/sandboxes/flyio/): broker sprite as a service (wake routing via http_port, JSON env map), egress lockdown, a Sprites Connectors comparison, and notes for raw Fly Machines.
  • Broker bootstrap is the platform's own env machinery with a plugin secret-zero as the normal case, never an assumed .env.local.
  • CLI reference and the proxy running guide cover --expose, --url, token, --persist-ca, and env --full.

Validation

  • Tunnel round trips across all four Node/Bun broker × client combinations, the packed npm tarball under Node, and the compiled binary.
  • E2B: broker + agent against real sandboxes, with injection and response scrubbing confirmed through their ingress.
  • Fly Sprites: the guide's exact recipe end to end — service created over the REST API with --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-ca against 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.
  • Sprites SDK snippet checked against the published @fly/sprites API and run against a live sprite.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@theoephraim theoephraim changed the title Add E2B guide Remote-sandbox support for the credential proxy (+ E2B guide) Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • varlock 1.14.1 → 1.15.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@931
npm i https://pkg.pr.new/@varlock/1password-plugin@931

commit: 47f3dba

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).
@theoephraim theoephraim changed the title Remote-sandbox support for the credential proxy (+ E2B guide) Remote-sandbox support for the credential proxy (+ E2B and Fly.io guides) Jul 29, 2026
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size

⚠️ grows the bundle by 102.9 KB (+2.1%)

Metric main This PR Δ
Total dist 4819.2 KB 4922.2 KB +102.9 KB (+2.1%)
JS 1664.6 KB 1692.0 KB +27.4 KB (+1.6%)
Sourcemaps 3078.6 KB 3154.2 KB +75.5 KB (+2.5%)
Type defs 76.0 KB 76.0 KB

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.
@theoephraim
theoephraim merged commit 738b8d1 into main Jul 30, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant