Changelog¶
All notable changes to msks are documented here. The format follows
Keep a Changelog; versions are
tagged vX.Y.Z.
[Unreleased]¶
[v0.6.0] - 2026-11-02¶
Changed¶
devenv processes upon a macOS host (#707). Themsksdprocess is registered on Linux hosts only, so on a Mac the command exits at once (No processes defined) instead of supervising the dev daemon's platform refusal into a restart loop.msks-dev's refusal on a Mac points at the new Developing on a macOS host chapter, which documents both ways a Mac reaches a live daemon: the client over an ssh tunnel to a Linux dev host, and the product Lima host installed from a built wheel (msks-setup-macos-host --wheel).msks-setup-macos-hostwithout--port(#699). The run now talks through the forward the existing msksd VM already carries — the forward is fixed at VM creation, so the VM's own answer is the deployment's port — and finishes the verify pass, the client config, and the preset on it. A VM created with a non-default host port needs no recreation; an explicit--portthat disagrees with the existing VM's forward stays a named refusal naming the recreation remedy.
Removed¶
- The
mount_sharesconfig feature (#685). Host paths are the one share surface: themount_shareskey andMSKSD_MOUNT_SHARESare removed,msks create --share/msks mount HOST:GUEST[:MODE]are the one grammar, and the TUI's mounts editor takes host paths only. A config that still names the key or the env var fails at load with a message pointing atmount_host_rootsand--share; workspaces whose stored rows carry named mounts drop them at read and at the next set (they mount nothing for them at the next start) — recreate those mounts with host paths.
Added¶
-
Version skew between the client and the daemon is named (#706). Every
msksinvocation but theimagegroup (whosecheckmember runs offline) makes one best-effort/healthroundtrip and prints a notice naming both versions and the remedy the skew's direction asks for (#715): a newer release daemon names the client's update, a development build that leads names the tree it came from — never a rerun that would downgrade it. The setup tool's check pass grows a row pairing the VM's daemon version with the tool's own, and the bootstrap refuses — with the version pair named, ahead of the unit restart — when the daemon it installs is not the tool's build or rejects the config the same run writes, instead of crash-looping behind the 90s verify timeout (#704's shape). -
The macOS share stack documented (#675). The mounts chapter gains the macOS share geometry — a Mac folder crosses vz virtiofs into the daemon's VM under
/mnt/msks, then virtiofsd into the workspace — with the four contracts that cross with the tree: watchers on shared trees run in polling mode (file events do not cross virtiofs), case and Unicode normalization follow the Mac volume, anrwshare names the workspace its single writer (Mac-side writers honor none of its locks), and a volume shared into a running workspace ejects only after the workspace stops. -
Lima self-install in
msks-setup-macos-host(#699). A Mac without Lima now gets it throughbrew install limarun by the tool itself, so a stock Mac reaches a working msksd in one run; a Mac without brew names the Homebrew line and stops. -
msks-setup-macos-hostwrites the client config and a login agent (#689). The tool now writesmsks.yamlitself — the daemon alias pointing a baremsksat the forwarded port with the token file and CA beside it — and a launchd agent runslimactl start msksdimmediately on install and at every login, so the daemon answers now and after a reboot with no exports in the shell profile. An operator's ownmsks.yamlis kept untouched (the printed exports remain the copy-paste fallback);--checkreports both the config file and the agent, and--uninstallremoves the agent along with the VM. -
msks-preflightprints per-section durations (#690). Every gate section carries a wall-clock column and a total in the summary, a--collect-onlypass names an unimportable module in seconds before the suite runs, and the suite run itself carries a 40-minute wall-clock budget that fails loudly — a wedged run can no longer hang a pre-flight session indefinitely. -
msks-setup-macos-host(#676). The macOS host setup: a stock Apple-Silicon Mac reaches a working msksd in one run — the tool creates the msksd Lima VM from the shipped template (the invoking user's home mounted under the reserved prefix, the daemon's port forwarded on both loopbacks), installs the daemon inside it (the PyPI release matching the tool's version, or--wheelfor a build in hand), writes its config with host-share modeprefixand the user's home the default allowed root, the daemon's tool set (egress plumbing, volume trio, seed tooling) with its kernel routing setting and the pinned cloud-hypervisor release beside it, enables its service, and prints the client preset (MSKSC_URL/MSKSC_TOKEN/MSKSC_CAFILE— the CA written under~/.config/msks/).--checkruns the doctor rows (Lima at the template's floor, the VM running, the nested-virtualization marker, virtiofsd inside, the listener answering);--uninstalldeletes the VM. Every phase is idempotent; reruns preserve the token, the CA, and the daemon's state. -
msks image checkgrows thevirtiofsrow (#674). The guest-image capability check now probes the virtiofs filesystem driver a share mounts through: Debian's pinned kernels build the driver in; Fedora's image ships it as a module in the runtime closure — current images pass, and an image without it fails its own row with the named gap. A workspace that attaches shares on such an image reports the same named gap on the guest's console and cloud-init log at start (the per-boot mount installer checks the driver before its units come up). -
msks create --share HOST:GUEST[:MODE]and the host form onmsks mount/umount(#673). The client surface for #667's host-path shares: a repeatable create flag whose~expands against your home before the request, the same spec grammar onmsks mount, andmsks umountnames what leaves by host path. A refused share surfaces the daemon's gate message verbatim — which gate refused and the remedy. See the mounts chapter. -
Host-path workspace shares behind the #667 gates (#672). A workspace's mounts now take a host-path form —
mounts: [{host: ~/code, guest: /mnt/code, mode: rw}]— alongside the named-share form: the daemon translates the path by its host mode (MSKSD_MOUNT_HOST_MODE,identityon a bare-Linux host orprefixon the Lima/macOS host), gates it against the roots its VM actually mounts and the allowed roots inMSKSD_MOUNT_HOST_ROOTS(both refusing at set time with their remedy: VM recreation, or the config edit), and refuses symlinks that resolve outside what the operator allowed. The host form defaults torw, the same default the named form carries — the trust decision (the share listed or the root allowed) precedes the mode. The virtiofsd lifecycle, the cloud-hypervisor--fswiring, and the guest mount units are the #620 machinery, keyed by a tag derived from the path. See the mounts chapter for the two gates and their refusal remedies. -
Lima template share roots under the reserved prefix (#671). The msksd-VM Lima template ships in the wheel (
msks/server/assets/msksd-vm.yaml) with every share root mounted under/mnt/msks— the WSL/mnt/cshape, so an operator's/Users/nameappears at/mnt/msks/Users/name— and the default root (the invoking user's home, writable) declared at VM creation. The host-share vocabulary inmsks.spec.mountscarries the prefix, the host-mode translation (identityon bare Linux,prefixon VM hosts), and the template validation that refuses a mount outside the reserved-prefix shape.
Fixed¶
-
A development-build tool no longer installs a release into the VM (#704). The tree's version now carries a
.devNmarker between releases, and amsks-setup-macos-hostrun without--wheelrefuses by name on the marker with the build-a-wheel remedy. Previously a tree whose version number matched a release silently installed that release's daemon, which rejected the config the same run wrote. -
A failed staging-agent bind reports the real error (#710). The ssh/rsync staging agent's cleanup closed a server a failed bind never built, so a bind refusal (a socket path past the AF_UNIX limit, an unwritable temp dir) printed an
UnboundLocalErrorabout the cleanup instead of the error itself. The cleanup now runs only for a server that was built, and the original refusal reaches the operator. -
msks-setup-macos-host --uninstallnames a missing limactl (#701). On a Mac wherelimactlis not on PATH, the uninstall arm died with a rawFileNotFoundErrortraceback; it now prints the install-Lima remedy on stderr and exits 1, the same named-refusal shape the setup and--checkpaths answer with. -
ssh re-pins host keys when a workspace's disks are recreated (#697). The daemon records a disk epoch on each workspace and advances it whenever it recreates the root overlay under a live row (a healed loss, a factory reset) — the guest regenerates its ssh host keys with the fresh overlay. The identity answer carries the epoch, the client rotates its per-workspace host-key cache on a change and prints a notice naming the recreation, and every invocation —
msks ssh <name>and the TUI's shell action alike — pins under the workspace's id, so a regenerated key no longer refuses from one entry point while the other silently re-pins. -
msks-preflightgoes green on a Mac dev host (#691). The remaining darwin failures in the gated suite are closed: the Linux-only surfaces skip with their platform reasons named, the host-independent paths they hid run on every host, and the suite's coverage bar adapts off Linux where those surfaces cannot execute. A Mac pre-flight runs the full gate stack green in about a minute; Linux CI is unchanged. -
The gated suite terminates on a Mac dev host (#690). The microvm driver's VMM and virtiofsd identity checks read the process command line through procfs only, so on a Mac every kill, shutdown, and launch sweep read its own spawned daemons as foreign — the signal never fired, the test waited forever, and a pre-flight run wedged past half an hour. The read now falls back to
ps(1)on darwin (Linux keeps the procfs path), the stale-socket probe sweeps a non-socket path that darwin'sconnect()answersENOTSOCKwhere Linux answersECONNREFUSED, and the Linux-only pieces (the NFQUEUE binding's import, a SIGSTOP escalation path the darwin event loop cannot schedule) skip with their reasons named. The suite drops from a hang to about a minute on an Apple-Silicon dev host. -
Guest images retry the home-volume mount on every boot (#688). A restarted workspace that lost the udev race — the volume's by-label symlink landing after the mount unit's 30s device timeout — came up with
/homeunmounted indefinitely (the first-boot retry that rides the identity seed never runs on a later boot). Every guest image now ships a udev rule that asks systemd for the mount when the labeled disk appears, so/homemounts moments after the label lands, on every boot a rebuilt image serves; existing images keep the seed's retry for first boots. -
The seed retries a dependency-failed home-volume mount (#686). On a boot slow enough that udev's by-label symlink lands after the fstab unit's 30s device timeout,
home.mountfails as a dependency and systemd never retries it — the volume disk is present, and first boot paid the seed's whole 20-minute wait (observed on the macOS nested stack). The seed's wait now asks systemd for the start itself when the volume disk is present: paced at 30s, best-effort, and only the start verb. The retry mount took under a second on every reproduction. -
doctor's apple-kvm row gates on the NV-mode marker (#664). The warning row now requires an NV-mode marker — the
kvm-arm.mode=nestedcommand line, sysfs's kvm mode parameter, or the kernel log's NV init line — beside the Apple MIDR, so a Linux VM under macOS's Virtualization.framework, whose silicon reads as Apple but whose nested KVM boots guests to ssh, runs the report clean; Apple-silicon hosts running KVM in NV mode keep the warning. See docs/doctor.md. -
First boot seeds the login user onto the
/homevolume instead of under it (#662). The first-boot seeding script waits for the persistent/homevolume's mount before writing anything under/home: on a host whose first boot is slow enough that the volume's first fsck+mount finishes after the seeding, the seeded homes and theirauthorized_keysused to land on the root filesystem and be buried when the volume mounted — the login user held ssh for the first minutes of first boot and lost it the moment the mount completed. A boot with no volume attached stands the wait down after a minute and seeds under the root filesystem's/home; a boot whose volume is attached but never completes its mount holds the wait's full 20 minutes first, then seeds the same way. -
The devenv shell opens on macOS hosts (#665). The dev shell evaluates and opens on aarch64-darwin: the Linux-only VMM toolchain (cloud-hypervisor and the netfilter family) joins the profile only on Linux, while the
msksclient, the Python/uv toolchain, and the lint gates are available on every host. A Mac checkout can now drive a remote daemon withMSKSC_URLstraight fromdevenv shell. Linux shells carry the same package set as before, unchanged. -
arm64 workspaces now power off on
msks stop(#649). The arm64 guest images' runtime module tree shipsgpio_keys, the input driver for cloud-hypervisor's device-tree power button, so the graceful-shutdown press reaches logind's power-key handling and the guest powers off within the deadline. The x86 ACPI button path is unchanged, andmsks image check'sacpi-shutdownrow now passes on arm64 hosts. - Release image channel (#659). A version release now
carries the previous release's
images.jsonfrom the moment it is created, so image imports keep working — pulling the previous images — while the release's guest images build (up to hours); before, the manifest fetch failed during that window. The manifest is replaced when the new images finish publishing.
[v0.5] - 2026-10-28¶
Changed¶
- virtiofsd 1.13.0 floor for host mounts (#655). Host mounts
need virtiofsd 1.13.0 or newer: the uid/gid translation and
read-only enforcement the shares run on landed upstream there,
and older builds refuse the daemon's arguments at start.
msksd doctornow reads the version (a below-floor binary warns naming the gap and theMSKSD_VIRTIOFSDoverride — Linux Mint 22 ships 1.10.0), and a mount start on a too-old binary fails with the same named requirement instead of an "exited at start" log tail. See docs/mounts.md.
Fixed¶
- Host share mounts: git loose objects no longer fail with
EACCES (#657). Creating a file without owner-write bits and
writing through the same open file descriptor (git's
loose-object path) failed on every virtiofs share. The share's
xattr remapping now denies
security.capabilityat the map and leaves the name unmapped, keeping file-capability clearing with the host kernel — and, whatever the daemon's privilege, a guest can neither set nor read the attribute through a share. Applies at each workspace start.
[v0.4] - 2026-10-27¶
Added¶
- Fedora 44 guest image (#651). A third catalog image family
builds beside Debian and NixOS:
msks-build-guest fedora(and the-x86_64/-aarch64flavors) produces thefedora:44archive from Fedora's pinned Generic cloud image, with the same agent toolchain, console, mounts, and egress contract the Debian image ships. The guest boots with SELinux disabled — the image build reconstructs the filesystem without SELinux labels, and the workspace's isolation boundary is the VM itself. See docs/images.md. - Apple Silicon host warning in doctor (#649).
msksd doctornames the Asahi-class kernel limitation where it exists — cpu0's MIDR names Apple — as a warning row: this kernel's KVM does not yet deliver guest interrupts, so workspaces start and their kernels boot, then stall before ssh answers; everything short of a guest boot works. The installation chapter documents the same host class, and the row stays off x86_64 and non-Apple arm64 hosts. - Per-arch channel images and arch-checked imports (#638).
The release channel publishes one row per flavor and arch:
images.jsoncarries anarchfield beside the kernel and size, built by per-arch legs and merged into one manifest.msks image importcompares the image's declaredarchagainst the host's own machine and refuses a mismatch naming both sides — a foreign-arch kernel would hang wordlessly at boot. An image without the field imports as before, andmsks image checkandimage infoprint the declared arch. - arm64 guest images (#637). The guest pipeline builds for
x86_64 and aarch64:
msks-build-guestfollows the building host (the-x86_64/-aarch64flavors pin it), the arm64 guests run 16K-page kernels (Debian's ownarm64-16kflavour; a kernel patch on the NixOS side) so nested KVM initializes on 16K hosts like the Asahi class, andimage.jsondeclares itsarch— images without the field import as before. See docs/images.md. msks-setup-fedora-host(#635). The Fedora deployment-host setup —msks-setup-debian-host's phases over dnf: the capability wrapper, the routing kernel settings, the dependency packages, the kvm grant, the systemd user service, and--uninstall's mirror.sudo "$(command -v msks-setup-fedora-host)"prepares a stock Fedora host, the Asahi remix included, end to end. See docs/installation.md.- The dev daemon finds its wrapper and uplink (#635).
msks-devresolves the setup tool's/usr/local/bin/msks-capsand the host's default-route interface by itself, so a baredevenv processes upstarts on a host NixOS does not manage —DEV_CAPSandDEV_UPLINKremain the overrides. - Host share mounts (#620). A workspace can mount host
directory trees into its guest: the daemon's config names the
shares (
mount_shares, reloadable on SIGHUP), a workspace mounts them by share, guest path, and mode (msks create --mount,msks mount/umount, or the TUI's mounts editor), and changes apply at the workspace's next start. Files present in the guest as the workspace's own user. See docs/mounts.md. - Guest timezone from the host (#633). A create captures the host machine's timezone and the guest's clock follows it — the seed's first-boot script sets it, with no operator action. The capture is create-time and immutable: a host timezone change later affects only new creates, and a host on UTC leaves the guest on the image's own default. See docs/images.md (first-boot provisioning).
[v0.3] - 2026-10-24¶
- The guest images' pi coding agent moves to 1.4.0, and a default
~/.pi/agent/settings.jsonnow ships for every user: thinking blocks stay out of the transcript and pi's install telemetry beacon stays off (#616). The project adopts the MIT license (#612), and the docs split the egress consent chapter out of guest networking.
[v0.2] - 2026-10-23¶
- Guest image updates, docs changes.
[v0.1] - 2026-10-18¶
Added¶
- The NixOS workspace guest image (#250). A second catalog image
built from the devenv-pinned nixpkgs with
msks-build-guest nixos(into.devenv/state/guest-nixos;GUEST_NIXOS_DIRrelocates it), registering under thenixosname besidedebian. The archive carries the same containerDisk contract and the same declared capabilities — cloud-init, the prelude-v1 console — so the daemon serves it with no new machinery and nothing keyed off the image's name. See docs/images.md. msks egress tui(#195). The consent decider TUI: a textual app that registers as the workspace's decider, shows held requests with countdowns (a/dverdicts,A/Dduration picker), and adds a rules screen with revocation — over the frames and endpoints #69 serves, reconnecting with backoff (reset after each healthy connection). Countdowns follow the deadline the daemon carries on each request frame, and a registration the daemon rejects (an unknown workspace) exits with the reason instead of waiting. Thewatch/decide/revokesubcommands remain the scripting surface. See docs/cli.md.- Placeholder secret store,
msks secretcommands (#198). Mint a sentinel for a workspace (msks secret mint <ws> --name <label> --dest <hosts> --secret-file <path>|-) and the real secret lives in a SecretSpec-backed store, provider as a setting (secret_store_provider:filedefault,agefor at-rest encryption,awssm,bws), never in the workspace;ls,renew --ttl,revoke, andcheckround out the surface. Secrets ride stdin or a file, never argv; each lifecycle event (mint, revoke, expiry) lands in the audit table. See docs/secrets.md, the key reference, and cli.md. msks rsync(#190). Zero-setup file copies over the forward:msks rsync <ws> -- -av ./site/ root@:/root/site/boots the workspace when the daemon reports it as not running, stages the ssh identity in memory the waymsks sshdoes, and runs the host rsync through a forward the copy opens itself — push and pull, with the identity in memory only and no key file written. A path with an empty host (:src/,root@:/root/site/) targets the workspace the command names, its login user riding the paths' ownuser@spelling; a--pubkeyworkspace exits naming the recovery for its private half. See docs/cli.md.-
Egress consent (#69). Workspaces pick a consent posture at create (
--egress-mode allow|static|interactiveplus repeatable--allowspecs;allowis the default and preserves the current unrestricted egress).staticresolves and reaches only the allowlist (off-list names answer NXDOMAIN and are recorded);interactiveholds each new connection's first packet in the deployment host's kernel until a decider answers over the events channel —msks egress watch/decide/revoke/rules/requestsdrive the whole verdict lifecycle (once/5m/15m/tilrestart/forever), with revocation also dropping a destination's live connections. Ports 53/853 drop toward anything but the daemon's resolver (the naming layer), verdicts pin names not just addresses, and the queue fails closed (msksd down or a full queue drops). New settingsMSKSD_EGRESS_MODE,MSKSD_EGRESS_CONSENT_TIMEOUT_S,MSKSD_EGRESS_CONSENT_RATE_LIMIT,MSKSD_EGRESS_CONSENT_RETENTION_DAYS,MSKSD_EGRESS_CONSENT_ROW_CAP,MSKSD_EGRESS_QUEUE_BASE,MSKSD_CONNTRACK_TOOL, andMSKSD_AUDIT_HMAC_KEY(consent rows carry HMAC tags when it is set). See docs/egress-consent.md and docs/cli.md. -
msks storageimage import times (#186). Each catalog row in the image cost table shows when the archive came in (local time to the minute) and theGET /api/v1/storageimage entries carry the time beside the hash, so entries sharing a reference (a rebuilt image imported again) read as distinct; the table orders them oldest-first. Re-importing the same archive refreshes the time. See docs/storage.md. msks resizeandPOST /api/v1/workspaces/{id}/resize(#184). A stopped workspace's disk sizes move after create: the/homevolume grows or shrinks (e2fsckquiets the journal,resize2fsmoves the filesystem, a shrink below used blocks answers a named409), and the root overlay grows with the next start's cloud-init filling the larger device. The row,msks ls --json, andmsks storagereport the new ceiling immediately, and a completed resize announcesworkspace.resized. New settingsMSKSD_RESIZE2FSandMSKSD_E2FSCKname the tools.msks storageandGET /api/v1/storage(#184). The daemon reports the state-disk budget (used/freewith a namedpressurecondition), each workspace's cost beside its size ceiling, and each catalog image's cost. The watcher publishes astorage.pressureevent on every change with a named log line, and workspace creates, image imports, and home-volume imports answer507belowMSKSD_STORAGE_FLOOR_MIB(default 512 MiB; the warn line sits atMSKSD_STORAGE_WARN_PCT, default 90), with imports sized when their bytes are knowable (the archive counted twice; aContent-Lengthbody counted once), instead of filling the disk until guests see I/O errors.-
msks ssh -- -Aforwards the operator's agent (#174). A command-line-A(or-o ForwardAgent=yes) now names the agent behindSSH_AUTH_SOCKexplicitly, sossh-add -landgit pushinside the workspace use the operator's keys — noHost msks-*ssh-config block needed. The workspace identity stays the session's authentication credential; the guest receives the operator's keys and nothing else. An explicit-o ForwardAgent=<path>keeps its own socket (it wins over every flag and value, as with stock ssh), and a line stock ssh resolves to off — including-A -a— passes through untouched. The explicit form needs an OpenSSH client of 8.2 or newer (the previous behavior forwards the session agent on older clients). -
Console challenge-response (#123, first half). The guest console demands a signature the daemon cannot produce: workspaces seeded with an identity now also plant an allowed_signers trust store, and each console connection carries a fresh nonce the client must sign (SSHSIG,
msks-consolenamespace) before any shell — verified by the guest's own ssh-keygen, refused closed otherwise.msks consoleanswers transparently with the same key as ssh (escrow, client data root, or the operator's ssh-agent, hardware keys included — msks never reads an agent-held half). Pre-change guests serve no challenge. Enrollment, minted-key rotation, and the audited re-enable follow in the second half. - Operator-supplied ssh key at create (#132).
msks create --pubkey FILE(or-on stdin) builds the workspace around a public key the operator already owns, at any well-formed key type: the daemon accepts the supplied line as-is (shape-checked, re-annotated, seeded like any identity) and holds no private half. Minted keys keep the FIPS-approvable type set; login uses the operator's own key, andmsks ssh's recovery text names it. Seedocs/networking.md. - Client-minted workspace identity as the create default (#121).
msks createmints the workspace's ssh keypair on the client and sends the public half only: the daemon stores and seeds that half exactly like its own minted one and holds no private half —msks keyanswers the public line, andmsks sshserves the private half from the client data root (~/.local/share/msks/<id>/identity, mode 0600).--daemon-mintkeeps the daemon-minted escrow mode (#111);--key-typeselects the type (ed25519default, #138). Seedocs/networking.md. - git-out through egress with a forwarded agent (#81). The
dogfood loop's outbound half is proven end to end by a new opt-in
root smoke,
test_local_egress_git_out(it runs in the KVM workflow's egress step): the workspace installs git from Debian's mirrors and fetches an unrelated HTTPS host through the NAT'd uplink, then pushes a commit to a scratch git server on the host through a test-widened input pin, authenticating only with an operator key that rode the forward as a forwarded agent — no credential in the image or the seed. The workflow and its ControlMaster caveat are indocs/networking.md. msks ssh(#112). One command logs into a workspace with stock ssh over the forward websocket: it boots the workspace if needed, fetches the minted identity over the authenticated API, and serves the private half from a transient in-process ssh-agent — ssh names the identity by its public half and signs through the socket, so the key never becomes a file on the client. Logs in as themsksworkspace user by default (-l rootfor recovery); per-workspaceknown_hostsunderaccept-new. Seedocs/cli.mdand the alias workflow indocs/networking.md.- Home-volume export/import (#80). A workspace's
/homevolume now moves through the daemon's authenticated listener as byte streams:GET/PUT /api/v1/workspaces/{id}/homestream the volume file out and replace it from an uploaded ext4 image, withmsks home export/msks home importon top (-speaks stdio, so the bytes compose with gzip or ssh). The workspace must be stopped (unknownrefuses too — the VM may be live), moves serialize against boots and deletes per workspace (waiters answer a named 409 afterMSKSD_MOVE_WAIT_TIMEOUT_S, and the daemon re-checks the live VMM under the lock), an upload that fails the ext4 check or dies mid-body leaves the existing volume in place, all-zero windows come back sparse, and each completed move publishes ahome.exported/home.importedevent. See storage and the CLI. - Minted workspace identity and
msks key(#111). msksd mints a per-workspace ssh keypair at create — Ed25519 by default (#115, #138), with the type configurable viaMSKSD_SSH_KEY_TYPE— and stores both halves with the workspace's state. The public half is planted intoauthorized_keysfor root and themsksworkspace user through the first-boot seed (composed with anyuser_datapayload), so a fresh workspace accepts ssh with no manual key steps.msks key <ws>fetches the identity over the token-gated API;--privateprints the private half,--outwrites it mode 0600. The halves persist across daemon restarts and workspace stop/start. - Guest sshd and rsync (#110). The workspace image serves TCP on
its forward path: sshd (Debian's own package, enabled) listens on
all interfaces behind a boot-time unit that waits for the NIC's
address (15s ceiling), with key-only login (
PasswordAuthentication no,PermitRootLogin prohibit-password) pinned by a config dropin, and rsync ships in the image. Host keys are generated on first boot into the persistent root overlay, so they survive stop/start — the recordedknown_hostsentry keeps matching. The egress input rules admit the conntrack-established replies to the daemon-originated forward dial; guest-initiated connections arrive state NEW and still drop. See networking. -
msks forwardand the forward websocket (#109). A workspace's guest TCP ports reach the client through a new authenticated endpoint,WS /api/v1/workspaces/{id}/forward/{port}: the daemon dials the workspace's deterministic tap address (retrying through the DHCP/service bring-up race) and pumps raw bytes both ways — one websocket per guest TCP connection, no framing, withforward.opened/forward.closedevents on the events channel. The client command bridges stdio (the ssh ProxyCommand shape) or, with--local PORT, a loopback listener where every accepted connection gets its own forward. The token authenticates through theAuthorizationheader — the REST surface's Bearer form, kept out of URLs and their logs; a workspace without egress is refused with the reason naming the missing NIC. Seedocs/cli.md. -
The dev-workspace bootstrap seed (#77).
scripts/dev-workspace.sh, passed tomsks create --user-data, bootstraps an msks development environment inside a workspace over its own egress NIC at first boot: uv (with its own Python 3.14), the repo checkout, anduv sync— the venv theunit-testsinvocation runs from — all on the persistent root overlay; stop/ start keeps it, a factory reset re-provisions, re-running is a no-op. Proved end to end by theTEST_EGRESS=1root smokes. -
msks shell --userand the console identity prelude (#63). The vsock console now serves shells as root or the image's workspace user: a guest-side helper negotiates the identity, the client terminal's size, and its TERM value in-band on each connection, and the listener accepts host-originated connections only (guest-local peers are refused). A workspace bound to an image whose record cannot be read is refused with close code 4501 — the console never silently downgrades to a raw root shell. Images declareconsole_protocol: prelude-v1andconsole_usersin their manifest; older images keep today's raw root shell through the daemon's legacy path. The helper is a Rust crate undersrc/console-helper, gated at 100% line and branch coverage (.github/workflows/rust.yml). - YAML config file for msksd (#46).
msksdreadsmsksd.yaml—--config <path>for an explicit file,--config=nonefor env-vars-only, and a baremsksdresolves$MSKSD_CONFIG_DIR/msksd.yaml(default~/.config/msksd/msksd.yaml), generating a commented template on first run. Precedence isMSKSD_*env vars > file > defaults; a config key is its variable minus the prefix, lowercased (MSKSD_PORT→port,MSKSD_EGRESS_SUBNET→egress_subnet— flat, klangkd's convention, derived by one rule so the spellings cannot drift), keys take native YAML scalars, and unknown keys, duplicate keys, or invalid values fail at startup. SIGHUP re-reads the file into the live settings without a restart. The deployment-host module (#229) generates the file for the service with the build's store paths, and a barealembicrun resolves the database URL from the default file when present. Seedocs/config.mdfor the key-by-key reference. -
msks-preflight(#43). One command delivers the Python-side pre-commit gates' complete feedback before the first commit attempt: every ruff and deferred-import finding, every xenon offender, the jscpd report, and — when sources undersrc/msks/changed — the gated suite run followed by every missing coverage line and branch arc for the changed files (scripts/covgaps.py). AGENTS.md makes it part of the loop: fix everything it names in one editing pass, re-run, then commit. The xenon and jscpd gates now also grade untracked-but-present files, so a new file grades from the moment it exists. -
Doubled-escape and chunked input in
msks shell(#36). The client reads stdin in 4,096-byte chunks and sends one websocket frame per chunk, so a large paste lands a few frames instead of one TLS frame per byte. Ctrl-] Ctrl-] — the second press within 50 ms, or both bytes in one chunk — sends one literal Ctrl-] (0x1d) to the guest; a single Ctrl-] still detaches, and bytes typed before the escape are delivered before the session closes. Seedocs/cli.md. -
Per-workspace
user_dataprovisioning (#41). A workspace created withuser_data(API field;msks create --user-data FILE,-for stdin, ≤64 Ki characters) runs the payload on its first boot through cloud-init: the daemon builds acidata-labeled iso9660 seed disk (NoCloud's own format,MSKSD_MKISOFS), attaches it read-only, stores it mode 0600, and deletes it with the workspace; both#!scripts and cloud-config documents run. The payload is create-time and immutable (PUT/PATCHanswer a named 405). Seedocs/images.md. -
Pre-commit hook suite (#72). Eleven hooks join the commit-time suite: the deferred-imports checker (
scripts/check_deferred_imports.py, ported from klangk — imports live at module scope, with# allow-deferred-importas the escape hatch), shfmt, shellcheck, check-executables-have-shebangs, markdownlint, actionlint, trufflehog, nixfmt, check-toml, yamllint, and prettier.devenv shellwrites a generated.prettierignore; the lint rules live indevenv.nix; CI's lint workflow runs the whole suite (pre-commit run --all-files). trufflehog scans staged file contents and fails only on credentials that verify live (the stock git-history variant scans no commits at commit time). The tree was formatted once to the new gates (docs, workflows, Nix, shell scripts). -
msks imagecatalog commands (#65). The CLI now manages the image catalog without curl:msks image lsprints one line per image (reference, short hash, default designation, kernel facts;--jsonfor the raw listing),msks image import <path>registers a daemon-side archive and prints the imported reference (the help states the daemon reads the path — nothing is uploaded),msks image rm <ref>removes by any catalog reference form (name:version, bare name,name@hash, full hash, or a unique hash prefix) and surfaces the 409 naming a workspace that boots the image, andmsks image info <ref>prints one image's full record. Seedocs/cli.md. -
msks-jscpdtoken-clone gate (#71). The jscpd scanner (5.0.16, a pinned per-platform prebuilt binary fetched from npm — linux x64/arm64 gnu and macOS arm64/x64; unsupported platforms fail at eval time — ported from klangk #2904) joins the dev environment as both a report (msks-jscpd) and a pre-commit gate:scripts/jscpd-gate.shis the single definition of the invocation (tracked sources insrc/msks/msks,--min-tokens 70), shared with themsks-xenonpattern. A commit fails when any exact clone of ≥ 70 tokens exists among those sources; the backend baselines clean (0 clones), so the gate starts from zero. -
Workspace egress networking, on by default (#52). Workspaces are networked from creation: each boots with a virtio-net NIC onto a per-VM tap on the deployment host, takes its address, gateway, and the daemon's resolver over DHCP from a per-workspace /30 of
MSKSD_EGRESS_SUBNET(the slice recorded on the row, so the address is stable across restarts), resolves through the daemon's UDP forwarder, and NATs out the host uplink through a per-VM nftables table (forward: the guest's source out the uplink and established replies back, all else drops; input: DHCP and the resolver only, so guests cannot reach the daemon's own services) — create with"egress": false(msks create ws --no-egress) to boot NIC-less. The plumbing arms whileMSKSD_EGRESS_ENABLED=trueand the daemon holdsCAP_NET_ADMIN; the host's forwarding comes from the msks module (#229). The guest overlay ships the DHCP client (systemd-networkd unit + resolved stub). Consent gating also arrives with #69. Seedocs/networking.md. -
Client CLI:
msks stop,msks rm, and the workspace listing namedmsks ls(#66).msks stop <id>powers a running workspace off — a graceful, deadline-bounded guest shutdown — and prints<id> stopped;msks rm <id>…deletes one or more workspaces (stopped or running) together with their persistent root overlay and/homevolume, printing one<id> deletedline per id. The workspace listing command ismsks ls(the#59behavior under its final name, matchingmsks image ls), completing the lifecycle set: ls / create / start / stop / rm / shell. Missing workspaces (404), workspaces recorded on another host (409), and missed shutdown deadlines (503) print one readable line and exit non-zero. Seedocs/cli.md. -
Client CLI:
msks ls,msks create, andmsks start(#59).msks lsprints one line per workspace (id, status, image hash, host;--jsonfor a machine-readable document), andmsks createPOSTs the API's create body —--imagefor a catalog reference,--cpus/--mem-mib/--root-mib/--home-mibfor sizing, explicit--kernel/--rootfsto bypass the catalog — printing the new workspace's id;--startalso boots it, andmsks start <id>boots an existing workspace later.msks shellnow boots a not-running workspace itself before attaching (notices on stderr). All commands use theMSKSC_URL/MSKSC_TOKEN/MSKSC_CAFILEconventions frommsks shell; connection failures, timeouts, and API or validation errors print one readable line, not a traceback. Seedocs/cli.mdfor the command and environment reference. -
Per-workspace persistent state (#14). Every workspace owns two persistent artifacts created with it: a root overlay (qcow2, copy-on-write over its image — package installs and root edits persist across
stop/start) and a/homevolume (ext4, labeledmsks-home, mounted at/home). Both survive stop/start and are removed withdelete; the newPOST /api/v1/workspaces/{id}/resetdrops only the overlay; sizes come from the create request (root_mib/home_mib) withMSKSD_ROOT_MIB/MSKSD_HOME_MIBdefaults, and the workspace row records the owning host, so a start/stop/reset/delete from another host answers a named 409. The shipped image boots its root read-write through the overlay. Seedocs/storage.mdfor the layout, lifecycle, and env-var reference. -
Workspace boot performance (#37). Start → interactive shell is now ~3.1s p50 on the reference host (was 6.7s; goal < 5s), measured by the new
scripts/perf-boot.pyharness, which also reports the VMM's peak resident set (165–185 MiB for a 1024 MiB guest). The shipped image boots Debian's cloud kernel (ext4/virtio-pci built in) with a msks-built minimal initramfs (one module, ~0.05s) and starts the vsock console before the boot completes; AppArmor, networkd, timesyncd, resolved, unattended-upgrades, and e2scrub units are off the boot.docs/boot-speed.mddocuments the breakdown and the measurement. -
Workspace image guide (#38).
docs/images.mddocuments the image contract (container-image tar, containerDisk layout,disk/image.jsonschema 2), building an image (the shipped builder and a from-scratch outline), registering viaPOST /api/v1/images(default designation, storage cost, removal rules), and referencing images from workspace create (name:version, bare name,name@hash, bare hash). -
Workspace image catalog with containerDisk images (#40). The canonical image is now a container-image tar (
podman loadcompatible) in the containerDisk convention (workspace-<name>-<version>.tar:boot/vmlinuz,boot/initrd.img,disk/rootfs.ext4,disk/image.jsonschema 2) — importable with stock tools.GET/POST /api/v1/imageslist and import (per-hash boot-file cache; workspace launches never unpack); workspace create accepts"image": "name:version"(or bare name, or hash) and falls back to the designated default, so a bare create works on a fresh deployment —MSKSD_DEFAULT_IMAGEimports and designates at first boot. Explicitkernel/rootfsfields still win. -
Workspace shell (#21).
msks shell <workspace-id>gives an interactive shell inside a running workspace microvm, from any host that can reach the daemon: the client speaks the authenticated/api/v1/workspaces/{id}/consolewebsocket (TLS + token, Ctrl-] detach, raw tty mode), and the daemon proxies it over virtio-vsock — the VM's vsock unix socket after aCONNECT <port>handshake — into a per-connection busybox ash on a pty served by static socat in the guest (root shell today, per #5's guest userland;MSKSC_URL/MSKSC_TOKEN/MSKSC_CAFILEconfigure the client). The guest assets gained the vsock module,/dev/vsockcreation, devpts/ptmx setup, and socat; the daemon retries the console handshake across the guest's post-boot bring-up window.
Breaking¶
- Dev state moved under
.devenv/state/(#156). The repo-root state dirs —.guest/,.msksd/— now live at.devenv/state/{guest,msksd}/, each relocatable viaGUEST_DIRorMSKSD_STATE_DIR(the daemon's own setting; use an absolute value so the tasks and the daemon land in the same place). Existing state does not migrate: move the directory you want to keep or rebuild (devenv processes up -d,msks-dev-ready), then delete the old dirs — they are no longer gitignored. The devenv shell also prunes its own stale one-shot wrappers (.devenv/shell-*.sholder than an hour) automatically.
Changed¶
- The NixOS deployment host (#229). msks ships as a flake
whose NixOS module runs msksd and cloud-hypervisor directly on a
NixOS deployment host; workspaces boot under cloud-hypervisor on
the host's
/dev/kvm— one VM boundary between a workspace and the metal — and egress consent is enforced in the host kernel (per-VM taps, the nftables/NFQUEUE consent path, DHCP, DNS, and NAT arrive with the module). The appliance layer (msks-appliance-*, the #220 update channel, the state-disk machinery) and the Kubernetes deployment target are removed: updates and rollback come from rebuilding the host's NixOS configuration. The roadmap of the time (docs/ROADMAP.md, since removed) carried the plan. -
Plain scripts instead of dependency-free devenv tasks (#166). Every msks devenv task without
after/beforeordering is now a plain script on the devenv shell's PATH (msks-build-guest,msks-preflight, and the rest) — run it from a devenv shell directly ordevenv --quiet -O dotenv.enable:bool false shell -- <name>from outside;msks:uv-syncremains the one task. The pinned nixpkgs source is exported asGUEST_NIXPKGSto every devenv context, and the build and lifecycle scripts print their start and outcome. -
Minted identity keys default to
ed25519(#138). The client mint (--key-type,msks create) and the daemon mint (ssh_key_type/MSKSD_SSH_KEY_TYPE) now mint Ed25519 keys — FIPS-approvable (FIPS 186-5) and accepted by ssh clients restricted to the commonssh-ed25519,ssh-rsaset, somsks sshinto a default-created workspace needs no algorithm passthrough.--key-type ecdsa(the former default) and--key-type rsaremain available for deployments whose validated crypto module predates EdDSA. Seedocs/networking.md. -
msks console(#117). The interactive workspace command is renamed frommsks shelltomsks console, matching the/api/v1/workspaces/{id}/consolewebsocket it speaks. A hard rename:msks shellnow exits with the standard unknown-command error. Seedocs/cli.md. -
msksd runs as a non-root service user (#101). The msks module's systemd unit executes the daemon as a dedicated
msksdsystem user holding exactly two ambient capabilities —CAP_NET_ADMIN(taps, nftables, and the VMM's tap opens) andCAP_NET_BIND_SERVICE(DHCP 67, DNS 53) — and nothing in its process tree runs as uid 0;/dev/kvmreaches it through thekvmgroup, and the state directory is service-user-owned (/var/lib/msksd).net.ipv4.ip_forward=1is a host setting the module carries when egress is enabled: msksd verifies it and refuses egress with the cause namingnet.ipv4.ip_forwardwhen it reads0. Seedocs/networking.md. -
The workspace guest boots Debian's generic kernel (#96). The workspace image archive shrinks ~19 MiB compressed (147.8 → 128.7 MiB xz) as Debian's full cloud module tree leaves in favor of the twelve-file runtime closure the build derives from
modprobemetadata (and pins by comparing the full tree's closure against the shipped tree's at build time). Boot speed holds (p50 start→prompt 3.24 s against the cloud flavor's 2.95–3.26 s host spread; goal < 5 s), and guest memory at first prompt is unchanged (~140 MiB); numbers recorded indocs/boot-speed.md. -
The workspace image ships cloud-init (#41). The image is built from Debian's
genericcloudcloud image instead of the cloud-init-freenocloudvariant: cloud-init and its python3 runtime arrive with the base (~130M larger), and two dropins pin NoCloud as the only datasource and keep cloud-init off the guest's networking. The interactive boot budget is unchanged (vsock shell ~3.0s p50); existing workspaces keep the images they were created with — recreate workspaces to move them onto the new image. -
Workspace stop is now a clean poweroff (#14). The local backend's stop presses the ACPI power button (
vm.power-button) and the guest's systemd-logind runs a full shutdown before the VMM exits. The endpoint stop used before was cloud-hypervisor v52's hard stop: the guest was never notified, and with persistent disks every stop dropped the writes still sitting in the guest's page cache. -
The workspace guest is Debian 13 trixie (#30). The rootfs comes from Debian's official nocloud cloud image (pinned by dated URL + sha512): systemd as PID 1, apt (present but inert while the root is read-only), and Debian's own kernel direct-booted.
msks-build-guest, the manifest contract, and the smoke path are unchanged; the busybox guest is gone. See the README for build details, timings, and the setuid/ownership notes. -
Local-driver and TLS hardening fixes from the first e2e (#10). The local driver now honors the absent-VM contract on stop/kill (a stale api socket behind a dead VMM reported ECONNREFUSED and delete-after-stop 500'd), the rootfs disk is declared
readonly+image_type: Rawinvm.create(v52's autodetection otherwise disables sector-0 writes, and O_RDWR on the read-only store share fails), andtls._writeloopsos.write(a short write through virtio-backed storage left a truncated CA key). -
The
msksddaemon and its/api/v1API (#8). msksd now serves versioned endpoints over one HTTPS+WSS listener — public health, hashed bearer-token auth with revocation (MSKSD_BOOTSTRAP_TOKENseeds the first credential), workspace create/list/status/start/stop/delete driving cloud-hypervisor through the microvm seam, and a websocket event channel for lifecycle transitions. TLS is operator-provided (MSKSD_TLS_CERT/KEY) or a self-signed CA generated on first run whose fingerprint is logged for trust-on-first-use pinning;--no-tlsserves plain HTTP for development. State lives in an SQLite database under the state dir (MSKSD_STATE_DIR), managed by Alembic migrations. -
Nix-built guest assets (
msks-build-guest,msks-demo-vm). The devenv now produces everything needed to boot a microvm — kernel, initrd, read-only ext4 rootfs into the guest state dir (.devenv/state/guest/) — from the nixpkgs revision devenv itself pins, on any Linux host with nix; the manual-download flow is gone. Boot tests pick the built artifacts up automatically (explicitTEST_VMLINUX/TEST_ROOTFS/TEST_INITRDvariables keep precedence) and skip themselves when the guest was never built or/dev/kvmis unusable.msks-demo-vmboots one interactive VM from the artifacts withch-remoteready (#5). -
msksd --reload(development, #144). The daemon gains a development flag that watches the msks package tree it runs from and restarts the process when it changes.
Fixed¶
-
Console websocket closes protocol-clean (#217). The console bridge now ends every session with a close frame instead of dropping the TCP: a guest that refuses the console signature closes with 4403 and the guest's
MSKS ERRtext as the reason (watched until the helper's AUTH OK verdict, so a logged ERR line in later shell output is not one), a guest stream that ends (shell exit) closes with 1000, and only a client disconnect ends without one. Clients can tell refusal apart from transport death. -
The nix-built msks package ships
textual(#203). The package set shipped withouttextual: the msks wheel build failed its runtime dependency check, and a daemon built from it crashed at import time.nix/textual-pkg.nixbuilds the pinned 8.2.8 wheel into the closure, and a new test keepsnix/msks-pkg.nixmirroring[project.dependencies]— the next dependency addition fails a unit test instead of the nix build. -
Guest images keep the base image's file ownership (#179).
aptinside a workspace now sees the stock Debian metadata the base image ships:mandb's cache isman:managain (anapt upgradeprintedPermission deniedon the whole man cache), and every other non-root owner — the shadow-group password files,utmplogs, thesystemd-network,dbus, andpolkitstate directories — round-trips with it. Build-time pins fail the image build whensudoloses its setuid bit or the man cache loses its owner. Workspaces must be recreated to pick up a rebuilt image. -
Workspace home and the guest's account list (#171). The first-boot identity seed now creates
/home/msksowned by the workspace user and populates it from/etc/skel(a home left root-owned is repaired on the next provisioning), and the guest image ships a cloud-init dropin that keeps the Debian cloud image's own default account (debian, with/home/debianand a passwordless-sudo entry) from ever being created — a workspace created from this image carriesmsksas its only account. -
msks sshwaits out a just-booted workspace's identity seed (#168). The firstmsks sshagainst a freshly created workspace could exitPermission denied (publickey): the guest's sshd answers before first-boot provisioning has writtenauthorized_keys, and a bare retry succeeds. A session whose pre-flight booted the workspace now probes the login (a throwawaytruecommand, retried up to 30s) before opening the real session, which runs exactly once either way. -
Workspace
sudoworks — the guest image keeps the setuid bits (#169). The image build extracts the Debian root unprivileged, which dropped every setuid/setgid bit (sudo,su,mount, …); themsksworkspace user hit sudo's refusal — "must be owned by uid 0 and have the setuid bit set". The build now restores the bits from the source image's own inode modes, and the image grants themsksuser passwordless sudo (the locked password makes NOPASSWD the only form that can run), sosudo apt updateworks over a workspace console or ssh. Workspaces created from an older image keep their old backing rootfs until recreated (msks rm, thenmsks create). -
Failed starts keep an actionable status (#158). A
vm.bootfailure reaps the half-created VMM and the watcher's dead-socket probe consults the identity check, so the workspace reportsstopped— not the incident'sunknown, which a bare pid check satisfied with a recycled pid — and the nextmsks startretries. The incident's exact shape, recycled-pid trigger included, is pinned by tests, and a stop/start lands a merged fix. -
Stale workspace sockets no longer break start (#151). A VMM killed without cleanup (host crash, hard stop) left
api.sock/vsock.sockbehind undervms/<id>/; the next start died binding them and surfaced a misleading unreachable-API/ENOENT 503. The start path now sweeps refused sockets (a live listener is never unlinked), removes the stalech.pid(a recycled pid after a host reboot no longer blocks starts with "already exists"), and a VMM that dies at spawn names its own cause in the error (the tail of its ch.log) instead of a bare exit code. VMM liveness is identity-checked against/proc/<pid>/cmdline: a pid that is not this workspace's VMM is never signaled, and one owned by another user is named in the error instead of silently reported stopped. - Legacy CA crash-loop on leaf remint (#148). A CA minted before
the strict-clean change parses but carries no Subject Key
Identifier; the leaf remint that a host change triggers read the
missing extension and exited, and
Restart=alwayslooped the daemon forever (observed live on a long-lived state disk after #146 changed the bind host). Such a CA is now replaced wholesale — a stderr line names the replacement beside the new fingerprint — instead of crash-looping. Current strict-clean pairs keep leaf-only remints (same CA fingerprint). - Strict-clean minted TLS certificates (#141). The CA and leaf
msksd generates now carry Subject/Authority Key Identifiers and
the CA a
keyCertSignKeyUsage, so Python 3.14 clients — whose default SSL context enablesVERIFY_X509_STRICT— verify the pair instead of rejecting it. Existing state directories keep their current pair; removing themsks-ca*.pem/msks-cert*.pemfiles under the state dir lets the next start mint the strict-clean replacement. - Mid-session console stalls no longer wedge a session open (#103).
The guest console helper's byte pump blocked on whichever direction
stalled first, so a wedged vsock transport froze the whole session —
input echoed but never executed, no output, no close. The pump now
moves each direction independently and a stream that accepts no
writes for 300 s ends the session; on the daemon side,
console_stall_timeout_s(MSKSD_CONSOLE_STALL_TIMEOUT_S, 60 s default,0disables) closes a console websocket with 4502 when client input draws no guest bytes for the window —msks shellnames the close, and reconnecting opens a fresh session. The echo-off caveat (password prompts) and the helper-window relationship are documented indocs/cli.mdanddocs/config.md. -
Egress DHCP/DNS under uvloop-running daemons (#144). The development venv pulls uvloop (via
uvicorn[standard]), which does not implementloop.sock_recvfrom/sock_sendto; egress workspaces created by such a daemon never received a DHCP lease or DNS answer. The DHCP and DNS services now receive datagrams through a loop-portable helper and behave identically under asyncio and uvloop. This was also the real cause behind the "guest NIC up but no frames" symptom recorded on #143 — not the container boundary. -
/homecould fail to mount on slow boots (#14). The guest fstab mounted the home volume by label underx-systemd.device-timeout=2s; that clock starts at sysinit job enqueue, before udevd runs, and on a first boot from a fresh overlay (every root read a copy-on-write miss) the udev label probe can exceed what is left of the budget —home.mountthen fails for the whole boot (nofailkeeps the boot moving and never retries). The device timeout is now 30s, so the mount rides out a slow coldplug; a boot with no volume at all waits the same 30s once and continues. -
Egress workspaces never took a DHCP lease on fast hosts (#36). The guest reaches multi-user immediately after sysinit, and a networkd that enumerates its NIC while udev is still renaming it (eth0 to ens3) never manages the renamed link — DHCP never runs. The guest image orders networkd after
systemd-udev-trigger.serviceandsystemd-udevd.service, so the interface name is final before the first enumeration. -
ws_urlquotes the token and workspace id (#36). The console URL interpolatesMSKSC_TOKENraw into the query string; minted tokens are urlsafe today, so this was theoretical, but any future charset with+,&,=, or%would have mangled it. The token now passes throughurllib.parse.quote_plus(a query value), and the workspace id throughurllib.parse.quotewith no safe characters (a path segment, where+would reach the server literally). -
Workspace cleanup and image import release their resources (#56).
cleanupnow stops the workspace's VMM before deleting its directory — tracked or pidfile, likekill— where it previously orphaned the process. Image import closes the container-image archive after unpacking; each import previously left the archive's file handle open until garbage collection. - Workspace shell input now echoes (#61). The vsock console's
guest pty ran with
echo=0,icanon=0while the client keeps the local tty raw, and the service environment'sTERM=dumbleft bash's readline off — so typed characters reached nothing that would show them. The pty is now a plain canonical terminal (echo=1,icanon=1) and the unit setsTERM=xterm: line editing and history work, and programs reading stdin directly get normal tty echo. The console ships in the workspace image; a workspace keeps the image it was created with, so delete existing workspaces and the old image from the catalog — new workspaces then bind the new image. devenv shellno longer runs the pre-commit suite at shell entry (#32). A devenv 2.3.x scheduler regression pulleddevenv:git-hooks:runinto the shell's task graph, so a failing hook (e.g. the xenon complexity gate) aborted shell entry before it opened — with no way to use the shell to fix the failure. The suite still runs ongit commitand as themsks-xenontask.