Crates keep their history: vm (lib, was shuru-vm), hanzo-vm (CLI package
and binary, was shuru-cli), vm-guest, vm-proto, vm-proxy, vm-sdk,
vm-store, vm-darwin, vm-linux; all at 0.1.0, Apache-2.0, hanzoai/vm.
Config is vm.json, data lives in ~/.hanzo/vm (HANZO_VM_HOME), env vars
are HANZO_VM_*, the guest runs as /usr/bin/vm-guest, and assets download
from hanzoai/vm releases. The TypeScript SDK is @hanzo/vm. NOTICE
credits shuru as the origin. prepare-rootfs.sh builds the image natively
on arm64 Linux with mke2fs -d and re-execs in Docker only on macOS.
Includes rustfmt over the tree and fixes for clippy 1.98 lints.
Co-authored-by: Hanzo Dev <[email protected]>
Substitution was a blind byte replace over the whole guest to upstream
stream, which failed two ways. A placeholder straddling two reads went
upstream unsubstituted, reproduced on an 80KB upload where 3 of 2000 leaked.
And substituting in a body changed its length while leaving the framing that
declared it, so any request carrying a secret in its body hung or was closed.
Measured across framings: with a secret the same length as the placeholder
every shape returned 200, and with a differing length only the header case
did. Since placeholders are a fixed 30 bytes and no real credential is 30
bytes, body substitution has never worked for anyone, and no comparable
sandbox proxy offers it.
Substitution now applies to the request line and any header value, scoped by
position rather than by a list of header names, so an unknown vendor header
works with no configuration and a credential in a query parameter is covered.
Bodies stream through untouched, so no declared length is ever invalidated.
This is deterministic rather than timed. An unterminated head is
unambiguously an unfinished request, where an arbitrary byte offset is not,
so no flush heuristic is needed at any throughput.
Framing is read but never rewritten, only to find where the next head begins
on a reused connection. Anything RFC 9112 6.3 flags as a possible smuggling
attempt, both Content-Length and Transfer-Encoding, conflicting lengths, an
unparseable head, CONNECT or Upgrade, stops substitution and degrades to a
byte tunnel. An intermediary that never re-frames cannot desynchronise its
upstream by re-framing wrongly.
Also cheaper than before: only heads are scanned, not every byte of every
body.
A live relay additionally re-resolves its secrets every 5s, so a rotation
reaches a connection that is already open instead of waiting for it to
reconnect.
RFC 0002 is rewritten to match, and records the evidence, the rejected
options, and a separate defect found on the way: substitution is one
directional, so an upstream that reflects a request returns the real
credential to the guest.
Verified end to end on real VMs: header substitution with a length-changing
secret, an unknown custom header, a 40KB POST body forwarded untouched, five
requests substituted on one reused connection, a placeholder in a body
carried through verbatim with a 200 instead of a hang, and no placeholder
leaked in any case.
Spec for backing a secret with a command instead of a host env var, so
short lived credentials (GitHub App installation tokens) survive a long
running sandbox session.
- provider contract follows credential_process: argv array, JSON on
stdout with an optional expires_at, no shell in the path of a secret
- resolution stays per TLS connection, which is where the substitution
list is already captured, so the diff is a resolver with a cache
rather than a lock free read in the relay loop
- command backed secrets fail closed; env backed secrets keep the
current skip behaviour so nothing that works today starts failing
Closes nothing yet; tracks superhq-ai/shuru#40.