@microsandbox/control-client
v0.7.6
Published
`ControlClient` is `Client<ControlProtocol>` over the shared framed engine. It sends a bounded `control.hello` directly, negotiates generation and limits, and exposes the original welcome frame. Use it when the peer is known to support framed control. It
Downloads
1,154
Readme
TypeScript control client
ControlClient is Client<ControlProtocol> over the shared framed engine. It sends a bounded control.hello directly, negotiates generation and limits, and exposes the original welcome frame. Use it when the peer is known to support framed control. It does not probe JSON or downgrade after failure.
The root entry is browser-safe and accepts a caller-owned byte transport or reconnectable connector. The /node entry accepts an existing Unix socket or Windows named-pipe path without renaming it.
import { GetMemoryState, MiB, SetMemoryTarget, typedMessage } from "@microsandbox/control-client";
import { connectFramedControl } from "@microsandbox/control-client/node";
async function inspectAndResize(path: string) {
const client = await connectFramedControl(path, { setupTimeoutMs: 10_000 });
try {
const response = await client.request(typedMessage("control.memory.state", {}));
console.log(response.type, response.raw.body);
const before = await client.requestTyped(new GetMemoryState());
const accepted = await client.requestTyped(new SetMemoryTarget(MiB(2048)));
console.log(before.target_mib, accepted.target_mib); // bigint, even for small values
} finally {
await client.close();
}
}Generation-1 checked helpers cover capabilities, memory state/target, CPU state/target, and ordered secret updates. Generation 2 adds GetRuntimeCapabilities, CreateCheckpoint, CreateDiskCheckpoint, CreateBranch, PauseRuntime, ResumeRuntime, GetPauseState, GrowRootDisk, and CompactDisks. They validate replies and retain the actual response on ControlClientError. Generic message requests return peer error frames directly. A target reply reports acceptance and observation; it does not establish guest convergence. Secret batches stop at the first failure and retain earlier completed changes.
Use connectControl for automatic discovery on the same endpoint. It sends the existing read-only JSON capabilities request, closes that exchange, and opens a fresh framed connection only after an affirmative CBOR advertisement. Setup has one total deadline across both connections. Malformed discovery, timeout, EOF without a reply, or a failed welcome never trigger fallback or replay.
connectControl(path)
|
+-- JSON capabilities --> valid legacy reply --> fresh JSON exchange per operation
|
+-- JSON capabilities --> advertises CBOR -----> fresh hello/welcome --> shared framed clientimport { GetMemoryState, MiB, SetMemoryTarget, typedMessage } from "@microsandbox/control-client";
import { connectControl } from "@microsandbox/control-client/node";
async function compatibleResize(path: string) {
const connection = await connectControl(path);
try {
const reply = await connection.request(typedMessage("control.memory.state", {}));
if (reply.kind === "cbor") console.log(reply.frame.raw.body);
else console.log(reply.reply.raw, reply.reply.value.get("memory"));
const before = await connection.requestTyped(new GetMemoryState());
const accepted = await connection.requestTyped(new SetMemoryTarget(MiB(2048)));
return { before, accepted };
} finally {
await connection.close();
}
}ControlConnection.connectConnector accepts a repeatable caller connector from the browser-safe root. The connection exposes the read-only generation-1 capabilities projection, the complete runtimeCapabilities discovery snapshot, mode, clone(), isClosed(), close(), request(), and requestTyped(). framed() returns the complete generic surface in CBOR mode and fails locally with unsupported_mode in JSON mode. Encoded messages likewise fail locally in JSON mode; the adapter translates only known, validated native operations.
When a framed peer selects generation 1, ControlConnection.requestTyped() sends generation-2 checked operations through their historical JSON form before any mutation is admitted. An unverified connector rediscovers the endpoint format first. This path is never a retry after a CBOR failure. The explicit ControlClient rejects those operations locally against a generation-1 welcome. Linux descriptor-backed branch creation remains outside ordinary CBOR because it requires SCM_RIGHTS; generation 2 covers direct local branching without descriptor transfer.
JsonReply contains the original response line, including its delimiter and whitespace, and a lossless Map of fields. JsonNumber.token retains each original numeric token; checked memory observations become bigint without passing through a JavaScript number. Ordinary requests return ok:false replies. Checked helpers raise legacy_remote with the original reply and unknown batch progress. Neither arbitrary diagnostic strings nor legacy errors are converted into structured CBOR errors.
For explicit legacy access, construction is inert and performs no discovery:
import { GetCpuState } from "@microsandbox/control-client";
import { jsonControl } from "@microsandbox/control-client/node";
async function inspectLegacy(path: string) {
const client = jsonControl(path);
try {
return await client.requestTyped(new GetCpuState());
} finally {
await client.close();
}
}The equivalent root API is JsonControlClient.fromConnector(connector, options?). Ordinary legacy request and response lines keep their existing size contract; only automatic discovery replies have a 64 KiB limit. Preparing a secret batch snapshots its entries without imposing the framed 4 MiB ceiling on JSON. JavaScript strings remain subject to garbage collection; explicit buffer clearing is not a guarantee that all copies have been erased.
A standalone automatic JSON connection rediscovers before each fresh operation because an endpoint path cannot establish process identity. ControlConnection.connectVerifiedConnector permits discovery reuse when the caller supplies a VerifiedControlConnector: connect must verify every connected peer against the saved OS process birth identity, and verifySession must check the active run and process before each operation. Both receive a deadline and cancellation context. Runtime replacement raises runtime_changed before mutation admission and closes the shared handle. This extension point does not implement an SDK database or OS identity verifier by itself.
MiB, GiB, KiB, TiB, and Mebibytes are exported from @microsandbox/types/size. Their arithmetic matches the SDK helpers. SetMemoryTarget validates integer precision and range before converting to the wire quantity; it also accepts bigint for full-width values.
All generic operations remain available:
| Need | Method |
|---|---|
| Native or encoded payload | request(typedMessage(...)), request(encodedMessage(...)) |
| Exact opaque envelope | requestRaw(flags, body) |
| Message or raw subscription | openStream(...), openStreamRaw(...) |
| Owned sender and receiver | stream.split() |
| Follow-up using an owned ID | sendOnStream(id, ...), sendRaw(id, ...) |
| Exact packet without a subscription | writeUnchecked(bytes) |
| Optional checked unary operation | requestTyped(request) |
| Shared connection ownership | clone(), close() |
import { ControlClient, encodedMessage, type ByteTransport } from "@microsandbox/control-client";
async function inspectExtension(transport: ByteTransport, payload: Uint8Array) {
const client = await ControlClient.connectTransport(transport);
try {
const reply = await client.request(encodedMessage("extension.inspect", payload));
return { name: reply.type, payload: reply.payload, original: reply.raw };
} finally {
await client.close();
}
}The default control request deadline is 30 seconds. Expiry abandons the local wait; it does not cancel remote work or retry it. ClientError.delivery distinguishes requests that were not admitted from unknown outcomes. Complete idle connections do not expire automatically. Explicit close closes every clone; JavaScript finalization is only best-effort cleanup.
From packages, run npm run build, npm run typecheck, and npm test. Live validation is deliberately opt-in: set MSB_CONTROL_TEST_SOCKET to a disposable framed runtime's socket and run npm test -w @microsandbox/control-client. Add MSB_CONTROL_TEST_MODE=cbor to run automatic discovery as well. The package serializes test files because live files mutate and restore the same VM's targets. A skipped live test is not platform or runtime compatibility evidence.
For a historical JSON-only runtime, set MSB_CONTROL_TEST_MODE=json and run only the compatibility live file with npm exec -w @microsandbox/control-client -- vitest run tests/compatibility-live.test.ts. It checks automatic and explicit JSON access, concurrent reads, full-width result types, and accepted memory/CPU targets, then restores the original targets. These running-runtime tests do not establish SDK launch compatibility.
The additional secret test requires MSB_CONTROL_TEST_SECRET naming a dummy fixture initially set to before and allowed for example.invalid. It exercises partial completion, malformed batches, and legacy JSON failures, then restores those fixture values.
