Conversation
…g that sets them
Step 1 of docs/design/2026-09-20-microvm-bootstrap-token-and-vsock.md section 8:
the additive wire vocabulary a microVM session's configuration needs, with no
producer and no consumer yet. Nothing in this change sets any of it, so every
existing message is byte-identical — which is what makes "controld rolls before
runners" true for the three steps that follow.
protocol/runner gains two omitempty fields on Spec (BootstrapToken, the
single-use capability that replaces an environment's decrypted secret values
for a microVM session; SecretNames, the names those values will arrive under),
the two method words the exchange rides (fetch_session_secrets,
mint_session_bootstrap), their protocol version, CapabilityMicrovmV1, and
BootConfig — the first control frame a microVM host sends its guest. The schema
lives beside Spec rather than in internal/relay because every field of it is a
field of the create it came from (the note's open question 4).
internal/relay gains ControlEvent.Cold on a suspending notice ("this is not a
freeze — flush, unmount, forget every delivered secret") and the boot_config
kind. Frame is untouched.
The tests are the compatibility claim rather than a restatement of the struct:
the golden create with no bootstrap is byte-for-byte the one
TestPublicRunnerWireShapes already pins, a Docker create still carries its env
values verbatim, a warm suspend notice still has no cold key, and an older
decoder reads a cold notice as the plain suspending it has always answered.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…trol plane Step 2 of the microVM bootstrap design's rollout: controld can now mint a session bootstrap token and answer the exchange, and nothing asks yet, because no runner announces microvm.v1. This is the step that makes "controld rolls before runners" true. createSpec keys the one new decision on the placement's runner's announced capabilities, which now travel with the placement rather than being re-read at dispatch. For a runner announcing microvm.v1 it leaves material.Environment's values out of Spec.Env, sets SecretNames to what it withheld, and mints a 32-byte crypto/rand base64url token whose sha256 is stored against the session with the create's placement generation and a 120-second expiry. Every other runner is dispatched exactly the Spec it always was — asserted as strictly as the microVM row, because that is the compatibility floor. Three rules that are easy to get subtly wrong, each with a test that states why: the mint is unconditional on microvm.v1 rather than conditional on there being secrets (a session with a creator has a non-empty Spec.Env either way, and the driver's refusal keys on values-without-a-token, so "mint only when needed" would refuse every scratch session on a microVM runner); a secret_ref spelled like an agent-home variable is dropped from SecretNames exactly as it is already dropped from Spec.Env, or the reservation would be defeated one hop later inside the guest; and a mint the store will not record fails the create, because the alternative is dispatching the secrets after all. The store seam is control.SessionBootstrapStore: a port of two methods, with the spend as ONE atomic step, because single use enforced by a read followed by a write is not single use. Both stores implement it — the in-memory one under its mutex, Postgres as a predicated UPDATE with a second read only to say which of the four refusals applies — and controlapp/repotest gains three cases so the two are pinned against the same contract rather than against each other. internal/controld grows the two arms. fetch_session_secrets spends first and resolves the environment's secrets afterwards through the same launchMaterial the scheduler dispatches with, so there is one decryption path and not two; mint_session_bootstrap is the cold-resume half and takes no session id, because the id is the one the placement guard already checked. Neither is gated on the capability: a runner that did not announce microvm.v1 was handed the values in Spec.Env already, so a second, weaker fence in front of the real one could only ever disagree with it. The refusals are four distinguishable sentences that name a condition and never a token, a value, or another session, and the log discipline is a test rather than a claim: one case captures this package's whole log output across a mint, a successful fetch and a replay, and greps it for the fixture secret and for every token that crossed the wire — then proves the grep ran over real output by requiring the session id to be in it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e session Step 3 of the rollout: the runner announces microvm.v1, the microVM driver opens a virtio-vsock control channel before the VM starts, and a guest's first frame on it is everything it needs to be a session. The first runner to announce the capability is the first session whose secrets are withheld — and by now the plane can answer for them. internal/relay gains NetConn, a Conn over a byte stream with newline-delimited JSON framing. A vsock connection is an AF_VSOCK stream in the guest and an AF_UNIX stream on the host, where the WebSocket this package grew up on delivered whole messages; everything above Conn — the hub, the session side, the control channel, the RPC dispatcher — is untouched, which is the whole reason the seam is there. Writes are serialized, because a Hub writes from every attached client's goroutine and from SendControl, and two concurrent writes to a stream corrupt it permanently for the reader. internal/driver: PUT /vsock with guest_cid 3 and a per-BOOT uds_path inside the session's own state directory, a listener on <uds_path>_1024 opened BEFORE InstanceStart (a listener created after it is a race whose loser is a session that boots and is never configured), and runner.BootConfig as the first FrameControl on whatever connects. session.json and buildGuestEnv's copy of spec.Env are gone entirely: the configuration is composed in memory and written nowhere, and the socket is the only file this path creates (0600). A cold resume gets a new VM, a new socket, and a new token, because the old one is single-use and fenced and because Firecracker's documentation warns one uds_path cannot be multiplexed across VMs. And the driver refuses a create carrying env VALUES with no token, naming the control-plane version. That is the compatibility table's "old controld + new runner" row, and it is a per-driver question with two right answers, which is why the twelfth RunContract subtest asserts both: Docker must keep accepting them (it puts them in an argv and nothing reaches disk, and a control-plane rollback must not take every self-hosted session down), and a driver announcing microvm.v1 must refuse. Subtest 5 now carries a token so it stays a shared subtest rather than becoming a refusal on one driver and a snapshot on the other, and the microVM snapshot manifest now records the boot configuration's keys too, so its strip assertion is about what the session was actually configured with. internal/runnerd: the /register handler's body becomes serveSessionConn, which the WebSocket door and the vsock door now share — so the two cannot drift into meaning different things, and the microVM door is the stronger of the two (tenancy §18 item 53: the session id is the socket path's, not a query parameter the runner believes). A create on a withholding driver carries no dial URL, because there is nothing to dial. A cold suspend on one tells the sandbox it is COLD before the VM goes; the Docker path sends nothing on a cold stop, exactly as it never has, and a test asserts the silence. Two things needed care. The runner now ORIGINATES a session RPC — the mint on a cold resume — so it keeps a pending table of its own and takes its ids from a disjoint space (the high bit), because both directions ride one connection for one session and a response to the runner's id 1 would otherwise be delivered into a sandbox waiting on its own. And the microVM-specific behaviour is keyed on the announced CAPABILITY rather than on the driver's concrete type, through two optional interfaces (CapabilityDriver, HostedDriver), so the claim the control plane acts on and the behaviour the runner performs cannot disagree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…opened Step 4, and the last: the transport seam behind dialLoop, the microVM boot over vsock, and the cold-suspend handshake from inside the guest. A session keeps the sessiond it booted with for life, which is why this is last for the microVM path and irrelevant for the Docker one. The seam is a func(context.Context) (relay.Conn, error), because that is all either transport needs. The Docker path is byte for byte what dialLoop did inline — the same URL, the same nil options, the same read limit, pinned by a test that asserts the request URI a runner actually receives. The microVM path opens AF_VSOCK to (2, 1024); everything above the seam — ServeSessionWithExec, the RPC dispatcher, the agent sync — is untouched, because all of it already took a relay.Conn. A microVM guest boots before it reads any environment at all, because it has none: it dials, reads its whole configuration off the first control frame, exchanges its bootstrap token for the secrets the create withheld, and applies both to this process's environment in the shape bootEnvFromOS already reads. Translating into that shape rather than threading the struct through is deliberate: prepareBoot, the agent sync, the exec runner's inherited environment and the agent itself all read exactly these variables on the other path, and a second way of saying the same thing is a second way for the two to diverge. The connection it bootstrapped on is the connection dialLoop serves — one channel, by design. The exchange happens when the configuration brings a token this session has not spent, and that one rule is both halves of "at boot and again after every resume". A bootstrap token is single-use: a sessiond that re-exchanged on every connection would be refused on its first redial inside a live VM and stay refused forever, and one that never re-exchanged would come back from a cold resume with nothing. A resume is exactly the connection that brings a new token, because runnerd mints one before it boots the new VM — so no second signal is needed, and a test drives a boot, a redial and a resume in one case to pin all three. A refusal, or declared secrets with no token, fails the BOOT CHAIN rather than this process: it becomes the chain's first stage, which only fails, so the existing machinery reports stage_failed with a readable tail and the agent is never exec'd. The tail names the count of undelivered names and the control plane's own condition — never a name, never a value, never the token — and a test greps it for all three. And `suspending` with cold: true is not a freeze. The sandbox acknowledges at once, flushes the agent sync (the last thing an agent wrote is usually the thing worth keeping), ends every exec, lazily unmounts /rainier/agents so the home's block device is consistent when it goes away, forgets every delivered secret by name, and only then answers ready. A warm notice is unchanged and keeps them, because a frozen session comes back with the same process and the same agent — pinned in both directions. Two things only a real host can test, and they are named in the PR body: that Firecracker forwards <uds_path>_1024 to the socket the driver is listening on, and that the guest kernel has CONFIG_VIRTIO_VSOCKETS and /dev/vsock. Everything above that is exercised here over a net.Pipe standing in for the conn, which is what the seam is for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… not a teardown An independent adversarial review of this branch found nine substantive issues. Every one is fixed here, with the test that would have caught it. **A sandbox could mint itself unlimited bootstrap tokens.** routeControl forwards any `req:<method>` a sandbox puts on its control conn, and mint_session_bootstrap was in that set: a compromised agent could ask for a fresh token whenever it liked, spend it, and repeat — an unbounded, self-renewing capability to re-read its environment's current secrets, which is the property this design exists to remove. Single use and the 120-second TTL were enforced against nobody. controld cannot make this check — a session_req proves only that SOME runner sent it — so the runner refuses it, and refuses a request carrying an id from its own reserved space with it, since an id space the untrusted end can write into is not a space: a guest choosing 1<<63|n could have controld's echo delivered to a cold resume waiting on that number. The refusal is not conditional on the driver, because a Docker sandbox is on the same channel. **A refused exchange took the session off the air.** The control plane spends a token when it receives it, so a refused or timed-out exchange has spent it. sessiond keyed its "already done" on SUCCESS, so every later connection re-presented a dead token, the preamble failed, dialLoop treated it as a dial failure and backed off — forever. A session that was merely missing its secrets, and had already said so as a failed boot stage, became unreachable at the first conn blip. It now keys on ATTEMPTED, and a refused re-exchange on a live session is logged and the connection served, which is what reBootstrap's own comment always claimed. **relay.NetConn poisoned its deadline instead of failing.** A cancelled context set a read/write deadline and never cleared it, so a bounded write (connWriter.writeWithin's exec budget) left the conn readable and unwritable — a session silently dead upward while both ends believed it live. It now CLOSES the conn, which is exactly what *websocket.Conn does and what writeWithin's doc comment relies on. It follows that no caller may bound an operation on a conn it wants to keep, so the boot preamble closes and re-dials rather than handing on a conn it may have broken. **The boot exchange used id 1**, which is also the RPC dispatcher's first — so a late answer would be matched to the dispatcher's next call and decoded as an empty agent credential set. The comment claiming the counter was seeded past it was simply false. It is now numbered out of the dispatcher's space entirely, and clear of the runner's. **A failed cold resume made a session unresumable.** Firecracker binds `<uds_path>` at PUT /vsock and the driver binds `<uds_path>_1024`; only the second was ever removed, and the boot counter does not advance past a failed attempt — so the next resume recomputed the same path and bind failed EADDRINUSE on a socket nothing served, forever. Both paths are removed, at open and at close. **An unreadable placement made a token that could never be redeemed.** placedGeneration returns 0 when the read fails, documented as safe because "zero fences nothing" — true of an event, and no longer true of the number a token is minted against. A token at generation 0 against a row at 3 is refused as superseded by the very placement that minted it, and a guest cannot re-mint. It now reports whether it read anything, and a withholding create refuses rather than dispatching a session that cannot get its secrets. Every other create is dispatched exactly as before, which is asserted too. **Two tests were not testing what they said.** A runnerd case asserted that a hub it never created did not exist, with a failure message referring to an assertion that was not there; it now builds a sandbox and asserts nothing reaches it. And the microVM arm of assertStrippedFromImage could pass over an empty key list, so subtest 5 now carries a key it does NOT strip and the arm requires it to survive — if the driver stopped recording what a session was configured with, the strip checks would have gone quiet rather than failing. Also: a one-byte off-by-one made a frame of exactly maxFrameBytes writable and unreadable; the first dial now retries like every other one in the process rather than being the one whose transient failure is fatal; and four comments that claimed more than the code did (a seeded counter, a growing bufio buffer, an independent invariant check, a cleanup that removed one path of two) now say what is true. The store's refusal ORDER is written into the port contract, so two implementations cannot describe the same failure two ways. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jiashuoz
marked this pull request as ready for review
September 20, 2026 02:24
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.
Summary
PR 2 of the microVM driver sequence: the session bootstrap token and
virtio-vsock as the single host-to-guest control channel, implementing
docs/design/2026-09-20-microvm-bootstrap-token-and-vsock.md(merged onmain), which implements rainier-cloud ADR-0003 §2.7 items 1 and 2.Two things change, and they are the same thing seen from two ends. An
environment's decrypted
secret_refsstop being dispatched to a microVMrunner at all: the create carries their names and a single-use,
placement-fenced, 120-second token, and the guest exchanges it for the
values over the session RPC after it boots. And a microVM guest is configured
over vsock rather than by a file on the host or by MMDS: it opens one
connection, reads its whole configuration off the first control frame, and
runs its terminal, its session RPC and its lifecycle handshake over the same
conn, because
internal/relayalready multiplexes exactly those.Nothing about the Docker path changes. That is the compatibility floor, it is
asserted as strictly as the new path, and its own tests are untouched.
The commits
Four, landed in the note's §8 rollout order — ordered by who must be able to refuse
before anybody asks — each green on its own and each separately revertable.
1.
5daa85e—protocol/runnerandinternal/relay, additive only.Spec.BootstrapTokenandSpec.SecretNames;MethodFetchSessionSecretsandMethodMintSessionBootstrap;SessionBootstrapProtocolVersion;CapabilityMicrovmV1;runner.BootConfig(the note's open question 4: besideSpec, because every field of it is a field of the create it came from);ControlEvent.Coldandrelay.KindBootConfig. Nothing sets any of it, soevery existing message is byte-identical — pinned by repeating the golden
create literally, by a Docker create still carrying its env values verbatim,
by a warm suspend notice still having no
coldkey, and by an older decoderreading a cold notice as the plain
suspendingit has always answered.2.
6208bef— the control plane.createSpeckeys the one new decisionon the placement's runner's announced capabilities (which now travel with the
placement rather than being re-read at dispatch): for
microvm.v1it leavesmaterial.Environment's values out ofSpec.Env, setsSecretNames, andmints a 32-byte
crypto/randbase64url token whosesha256is stored againstthe session with the create's placement generation and a 120-second expiry.
control.SessionBootstrapStoreis the seam — two methods, with the spend asONE atomic step, because single use enforced by a read followed by a write is
not single use — implemented by the in-memory store under its mutex and by
Postgres as a predicated
UPDATEwith a second read only to say which of thefour refusals applies.
internal/controldgrows the twohandleSessionRequestarms;fetch_session_secretsspends first andre-resolves the secrets through the same
launchMaterialthe schedulerdispatches with, so there is one decryption path and not two.
3.
1e0e240—runnerdand the driver.relay.NetConn(arelay.Connover a byte stream, newline-delimited JSON, writes serialized because a Hub
writes from every attached client's goroutine).
PUT /vsockwithguest_cid3 and a per-boot
uds_pathinside the session's state directory; a listeneron
<uds_path>_1024opened beforeInstanceStart;boot_configas thefirst
FrameControlon whatever connects.session.jsonandbuildGuestEnv's copy ofspec.Envare deleted entirely. The driver refusesa create carrying env values with no token, naming the control-plane version.
runnerd announces
microvm.v1, shares the/registerbody with the vsockdoor (
serveSessionConn), leavesSpec.DialURLempty on that path, andoriginates
mint_session_bootstrapbefore a cold resume boots.4.
cb1b7de—sessiond. Afunc(context.Context) (relay.Conn, error)behind
dialLoop; the Docker path keepswebsocket.Dial(dial+"?session="+id)byte-for-byte (pinned by the request URI a runner receives); the microVM path
dials
AF_VSOCKto (2, 1024). The guest reads its configuration off the firstframe, exchanges its token, and applies both to its own environment in the
shape
bootEnvFromOSalready reads — soprepareBoot, the agent sync and theexec runner are identical on both paths. A refusal fails the boot chain.
suspendingwithcold: trueflushes, ends the execs, unmounts/rainier/agents, forgets the delivered secrets, and then answers ready.5.
94e34de— the review's findings. A fifth commit, because the reviewbelow is part of the work rather than a note about it. It is not a fifth step
of the rollout: every fix belongs to one of the four steps above and each of
those is still green on its own.
Compatibility (the note's §3 table)
controld rolls before runners, the order the fleet already uses.
microvm.v1announced, socreateSpecmints nothing, withholds nothing, dispatches today'sSpecexactly.TestCreateSpecWithholdsOnlyForMicrovm(four non-microvm rows)Spec.Envcarries the secrets. The microVM driver refuses to boot, naming the control-plane version — a refusal, not a quiet write tosession.json.RunContract's twelfth subtest;TestMicrovmRefusesSecretValuesWithNoTokenhandleSessionRequest'sdefaultarm answersunknown method …, and the guest then applies the row below.TestSandboxRequestIsAnswered(pre-existing, unchanged)SecretNamesis a clean boot; a non-empty one with no token, or a refused exchange, fails the boot chain asstage_failednaming the count of undelivered names and none of their values.TestBootOverVsockWithNoSecretsDeclaredIsACleanBoot,TestAMissingTokenWithDeclaredSecretsFailsTheBootChain,TestARefusedExchangeFailsTheBootChainWhat is tested here
plan names — fresh, replayed, expired, superseded placement generation,
another session's — live in
controlapp/repotest(cases B1–B3), so they arerun against the in-memory store and Postgres rather than against a fake;
and again end-to-end over the live runner plane in
TestFetchSessionSecretsTable.createSpec's "the values or the token,never both" is a matrix over announced capabilities.
TestTheBootstrapExchangeLeavesNothingInTheLogcapturesinternal/controld'swhole log output across a mint, a successful fetch and a replay, greps it for
the fixture secret and for every token that crossed the wire, and then
proves the grep ran over real output by requiring the session id to be in it.
TestMicrovmWritesNoDecryptedEnvironmentwalks thewhole state directory for the fixture secret and for the bootstrap token,
and checks the mode of every file and directory it finds.
and the assertions are on what it RECEIVES: the whole configuration, the
bootstrap pair, the proxy with the session's identity as userinfo, a
no_proxywith no dial host in it. The socket's mode is pinned at 0600.leaves the parked session's socket removed; a mint that cannot be made fails
the resume and leaves the session parked for a later one.
net.Pipe-backed fake transport. Boot config applied(in the shape
bootEnvFromOSreads), the exchange sent exactly once, arefusal failing the chain with a count and no name or value, a redial NOT
re-exchanging a spent token and a resume re-exchanging a fresh one, the cold
suspend forgetting the delivered secrets and the warm one keeping them.
What needs hardware
There is no KVM in CI and none on the machine this was written on, so four
things are asserted by construction and by unit test and are not yet proven
against Firecracker. They are Phase 1's host harness, not
go test:<uds_path>_1024to the unix socket thedriver is listening on, and accepts
PUT /vsockin the version and jailerconfiguration we ship (the runbook pins v1.17.0);
CONFIG_VIRTIO_VSOCKETSand/dev/vsock, andthat the session image sets
RAINIER_TRANSPORT=vsock;169.254.169.254while the session stillboots — which is the point of removing MMDS;
/rainier/agentsagainst a real blockdevice.
Everything above the socket is exercised here: the host end over a real unix
socket in
internal/driver, the guest end over anet.Pipeincmd/sessiond.The twelfth
RunContractsubtest"a create carrying secret values and no token is refused", and it is written
so that both drivers have something to fail:
microvm.v1must accept the create.Docker puts the values in a
docker runargv and nothing reaches the hostfilesystem; a refusal there would mean a control-plane rollback took every
self-hosted session down.
is precisely the claim the plane withholds on — a driver that announces it
and then accepts the values has told the plane it will do something it does
not do. The refusal must name the bootstrap token and
microvm.v1, must notquote the value, must leave no session behind, and the same create WITH a
token must be accepted (so the refusal is about the token and not about the
field existing).
The note's other half is also done: subtest 5 ("snapshot strips the named
environment keys") now carries a token, so it stays a shared subtest rather
than becoming a refusal on one driver and a snapshot on the other, and the
microVM snapshot manifest records the keys of the boot configuration as well
as of the driver's own map — so
assertStrippedFromImage's microVM arm readsthe published manifest for what the session was actually configured with,
rather than for a map that is now always empty. Tenancy item 38 is a test.
Gates
make verify(module-path, protocols, control, session-image-security-policy,tests, build, vet),
go test ./... -race -count=1,go vet ./...andgit diff --checkall pass.Skipped for lack of a database: every test in
internal/controld/pgstoreskips here with "docker not available andRAINIER_TEST_PG_DSN unset" — including
TestPGStoreRepositories, which iswhat runs
controlapp/repotest's three new bootstrap cases (B1–B3) againstthe Postgres store and the
0015_session_bootstrapmigration. Those threecases do run here against the in-memory store, and the two implementations are
written against the same contract for exactly that reason; the Postgres half
needs a run with a database.
Review
An independent adversarial review was run over the four-commit diff, the
design note and
internal/driver/driver.go, asking for correctness bugs,secret leaks to disk or logs, contract violations and vacuous tests. It
found nine substantive issues. All nine are fixed in
94e34de, each withthe test that would have caught it. In severity order:
routeControlforwards any
req:<method>a sandbox sends, andmint_session_bootstrapwas in that set — so a compromised agent could ask for a fresh token
whenever it liked and re-read its environment's current secrets forever.
Single use and the 120-second TTL were enforced against nobody, on Docker
sessions as well as microVM ones. controld cannot make this check (a
session_reqproves only that some runner sent it), so the runner nowrefuses the method from a sandbox, and refuses a request carrying an id
from its own reserved space with it — an id space the untrusted end can
write into is not a space.
token on receipt, so a refused or timed-out exchange has spent it;
sessiond keyed "already done" on success, re-presented the dead token on
every redial, and
dialLoopbacked off forever. A session that was merelymissing its secrets — and had already said so as a failed boot stage —
became unreachable at the first conn blip. It now keys on attempted, and
a refused re-exchange on a live session is logged and the conn served.
relay.NetConnpoisoned its deadline instead of failing. A cancelledcontext set a deadline and never cleared it, so
connWriter.writeWithin'sbounded exec write left the conn readable and unwritable — a session
silently dead upward while both ends believed it live. It now closes the
conn, which is what
*websocket.Conndoes and whatwriteWithin's owndoc relies on; the boot preamble consequently closes and re-dials rather
than handing on a conn it may have broken.
answer would be decoded as an empty agent credential set. The comment
claiming the counter was seeded past it was false.
<uds_path>_1024was removed, never<uds_path>— which Firecrackerbinds — and the boot counter does not advance past a failed attempt, so
the next resume hit
EADDRINUSEon a socket nothing served, forever.generation 0 against a row at 3 is refused as superseded by the very
placement that minted it, and a guest cannot re-mint. A withholding create
now refuses; every other create is dispatched exactly as before.
assertStrippedFromImagecould pass over an emptykey list. Subtest 5 now carries a key it does not strip, and the arm
requires it to survive — so the strip checks cannot go quiet.
failure message naming an assertion that was not there. It now builds a
sandbox and asserts nothing reaches it.
maxFrameByteswritableand unreadable.
Plus, from the same review: the first dial now retries like every other one
in this process rather than being the one whose transient failure is fatal;
the store's refusal order is written into the port contract so two
implementations cannot describe one failure two ways; the refusal for an
unwithheld create no longer implies every value in the block is secret; and
four comments that claimed more than the code did now say what is true.
The review came back clean on secrets to disk or logs (it checked every
new log line, error string, file write and the snapshot manifest, plus the
file modes), on Docker byte-identity, on the additive wire fields, and
on store atomicity.
Not in the design note
Everything below is a decision this change had to make that the note did not:
relay.NetConn. The note names vsock but not a framing, and a bytestream has none: one JSON value per line, chosen because every message here
is already produced by
encoding/json, which never emits a bare newlineinside one — so the framing needs no second encoder that could disagree with
the first. Bounded at 16 MiB, matching both WebSocket ends'
SetReadLimit.Writes are serialized, which a
*websocket.Conndoes internally and anet.Conndoes not.runner.BootConfig.Envand.NoProxy. The note's field list says "agentmanifest"; the map is that plus the per-provider home variables, which are
paths and names and cannot be enumerated statically (the provider table is
data).
no_proxyis the valuenoProxyForderives on the Docker path, kepton the wire so there is one source of truth rather than two.
runner.SessionBootstrapProtocolVersion. The note's payloads carry"protocol": 1and name no constant for it.control.SessionBootstrapStoreand its four sentinels. The note says"a small store seam"; this is its shape.
FleetOptions.BootstrapsisREQUIRED rather than optional, so a control plane composed without one
refuses to start instead of choosing between refusing every microVM create
and quietly putting the secrets back in
Spec.Env.controlapp/repotest, not acontrolappunit test, so it runs against both real stores.driver.CapabilityDriveranddriver.HostedDriver. Two optionalinterfaces, so the microVM-specific behaviour in runnerd (no dial URL, a
cold notice) keys on the announced CAPABILITY rather than on a concrete
type — the claim the control plane acts on and the behaviour the runner
performs then cannot disagree.
SetHostis a setter because the runner iscomposed over the driver and so cannot be a constructor argument.
sessiond --transport/RAINIER_TRANSPORT. A microVM guest receivesno per-session environment at all until its boot configuration arrives, so
which transport it has cannot be inferred from a create. It is a property of
the rootfs, and the session image sets it — which is image work in a later
PR of this sequence.
stageSecrets, firstin the chain, whose script only fails — so the existing watcher reports
stage_failedwith a tail and the agent is never exec'd, through exactlythe machinery a failed setup already goes through rather than a second path
that would have to be kept in step with it.
boot and again after every resume" and does not say how a guest tells a
resume from a redial. A resume is exactly the connection that brings a new
token, because runnerd mints one before it boots the new VM; a redial brings
the one already spent. Keying on the token needs no second signal and gets
both cases right — a
sessiondthat re-exchanged per connection would berefused on its first redial and stay refused forever.
mint_session_bootstrapis not gated onmicrovm.v1. A runner that didnot announce it was dispatched the values in
Spec.Envalready, so themethod grants it nothing new; a capability check there would be a second,
weaker fence in front of the one that holds (the placement guard, the hash,
the generation, single use), and two fences that can disagree are worse than
one that cannot.
both directions ride one connection for one session, so its ids come from a
disjoint space (the high bit) — otherwise a response to the runner's id 1
would be delivered into a sandbox waiting on its own id 1.
longer what is missing — the runner mints a fresh one — but the rest of the
boot configuration is, and persisting it is exactly what §2.7 item 1 rules
out. Rebuilding it from the control plane on a resume is the
portable-checkpoint work (ADR-0003 §2.3), not this change.
found. A runnerd restart leaves a RUNNING microVM session's control socket
unserved: the listener died with the process, and the driver cannot re-open
one because it has no boot configuration to hand whoever connects (that is
the same in-memory-only rule as above). The guest's
sessiondbacks off andredials forever, so the session keeps running and is unreachable until it is
destroyed. It is the same shape as the cold-resume refusal PR 1 introduced
and has the same fix — a create-shaped resume from the control plane — and
it is a strictly smaller gap than the Docker path has, since that one has a
listener a restarted runnerd re-opens.
unixPathMaxand short test directories. A unix socket path is boundedat 108 bytes including its NUL, and the path is composed from an operator's
--microvm-state-dir; the driver checks it and names the flag, because"invalid argument" from
bind(2)is not something anybody can act on.🤖 Generated with Claude Code