Releases: EstebanForge/construct-cli
Releases · EstebanForge/construct-cli
Release list
The Construct CLI 1.16.4
[1.16.4] - 2026-08-27
Fixed
- microVM: construct now matches msb 0.6.15 (schema, refs, workdir): construct embeds the microsandbox engine through the Go SDK, pinned at v0.6.10, while the host
msbCLI could be newer. A newer CLI migrates~/.microsandbox/dbforward and the embedded engine then fails every daemon create withdatabase schema is newer than this msb binary(hit live on a Linux host running msb 0.6.15). The SDK pin is now v0.6.15; when upgrading the host msb, upgrade construct in the same window. - No more phantom 3.5GB image transitions:
msb image tagdoes not exist in msb 0.6.15 (and the pull+tag flow was dead code), so a successful GHCR pull always "failed" its tag step and construct fell through to the docker save + load transition.msb load -ialso imports archives under thelocalhost/prefix, which the bare-ref probe never matched, so every run re-transitioned the archive. Image resolution now probes candidates in order (bare,ghcr.io/estebanforge/construct-box:latest,localhost/construct-box:latest) for EnsureImage, the daemon run spec, the prepull, andct sys doctor; the pull path verifies the registry ref and the load path verifies the localhost ref, with no tag step at all. Known follow-up: the docker-archive transition still produces an incomplete rootfs under 0.6.15 (guest has no/bin/sh); the registry pull is the reliable path and is preferred. - Daemon create no longer dies on image WORKDIR validation: msb 0.6.15 validates the image working directory at create time and construct-box declares
WORKDIR /projects, which the transitioned archive fails. Sandbox create now sets an explicit workdir (/home/construct, present in every construct-box image); exec paths set their own cwd, so this is only the default. - Boot telemetry is now collectable:
msb-boot:lines printed to stderr only, so the dogfood P0 greps overlogs/*.logmatched nothing even on a healthy install.msbLogBootnow also appends an RFC3339-stamped line to~/.config/construct-cli/logs/msb-boot.log; the format test isolates HOME so fixture lines cannot pollute the real log.
Docs
docs/VMsv2.mdsection 10: preliminary single-run observations recorded (macOS recreate 490s with the skills-drift recreate firing correctly, reconnect 0s on both hosts) plus the collection caveat that the log-file telemetry ships with this release.
The Construct CLI 1.16.3
[1.16.3] - 2026-08-26
Added
- Host skills mount: bind the host's skills library into every supported agent's skills dir:
~/Dev/EstebanForge/AGENTS/skills(or any other configured source) is now bind-mounted at create time into/home/construct/<agent>/skillsfor ten supported agents (agy, claude, amp, qwen, copilot, crush, droid, goose, kilocode, cline). Source resolution precedence:$CONSTRUCT_SKILLS_SOURCEenv var, then[sandbox] skills_source, then auto-detect (~/Dev/EstebanForge/AGENTS/skills,~/AGENTS/skills,~/.config/construct-cli/skills,$XDG_DATA_HOME/construct/skills). Mount is read-only by default; opt into read-write via[sandbox] skills_read_only = falsewhen an agent must author skills. Docker compose uses:ro(or:ro,zon Linux SELinux); microVMmsb.Mount.Bindhonors the same flag. Fails closed (no mount, no error) when the source does not resolve. Managed entirely by construct-cli;manage.shno longer needs to copy skills into the sandbox home. - Daemon recreate parity for skills mounts: a new label
construct.daemon.skills_hash(hash of source + read-only flag + target count) is stamped byBuildMsbRunSpecwhenever skills are enabled, andmsbDaemonNeedsRecreatechecks it FIRST in both multi-path and single-path modes. Toggling skills, flipping RO/RW, source appearance, or supported-agent-list growth now recreate the running daemon so the new mounts take effect (previously the change was invisible until manual recreate). P2.2 wire-in (combined mount-set resolution inmsbSandboxMounts+MsbPathMaps) is still pending; the hash surface is ready for it. - Boot telemetry (
msb-boot:log line):EnsureMsbDaemonnow emits a stablemsb-boot:line at every return path with the outcome (cold | recreate | warm | reconnect), elapsed seconds, mount count, and (forrecreate) the reason.msbBootClockis injectable so tests run deterministic without sleeping. Output goes to stderr (run-path rule respected). Numbers land indocs/VMsv2.mdsection 10 once the dogfood week collects medians; P6 (snapshot fork) is gated on those numbers. - Widened daemon flock (phase 1): a blocking
syscall.Flockon~/.config/construct-cli/daemon.lock(mode 0600) is acquired at the top ofEnsureMsbDaemonviadefer releaseLock(). The critical section now wraps read state, decide, write state, and the recreate/boot itself so concurrentctinvocations learning different roots cannot produce last-write-wins root loss or a double recreate. A 250ms "waiting for another construct invocation" notice fires on slow acquires.ct sys doctorsurfacesdaemon lock: free/held(per-request probe, intentionally not transactional with the actual lock). - Learned roots store (phase 2 data layer):
roots.jsonis a versioned, atomic-write JSON store at~/.config/construct-cli/roots.jsonwith LRU eviction ([daemon] max_learned_roots, default 8).requestLearnRootenforces the workspace guard, prompts via gum when interactive, denies with an actionable message when non-interactive. The fullEnsureMsbDaemonwire-in (P2.2) is pending; the helper is shipped withnolint:unusedand unit tests covering the no-op paths. - CLI surface for learned roots:
construct sys daemon roots list(table of root, source, last used, mount dest) andconstruct sys daemon roots forget <path>(refuses configureddaemon.mount_pathsentries). Pinned paths and forgotten paths are shown side by side; themanage.shsymlink/copy flow for skills stays in place as a fallback for non-construct run paths. - Idle stop (phase 3): nothing runs while the user is away. A session registry at
~/.config/construct-cli/sessions/<pid>.json(one file per livect) is read+written only inside the daemon flock critical section.execViaMsbDaemonregisters the current PID;Teardownunregisters and, on the LAST unregister, spawns a detachedconstruct sys daemon idle-watchprocess (true daemon viaSetsid, stdin/stdout/stderr closed,Process.Release). The watcher sleeps[daemon] idle_stop_minutes(default 45; 0 disables), rechecks the registry every 30s, and stops the daemon only if the count is still zero at the deadline. A newctinvocation that registers a session during the sleep stands the watcher down. The watcher acquires the flock before stopping (round 8 fix) so a freshEnsureMsbDaemoncannot be torn down mid-flight, and re-checks the live count under the lock. Bridges (SSH proxy, clipboard, host exec, herdr) die with the daemon, so idle = smaller host attack surface. - Background image prepull (phase 4): after a successful
construct update, a detachedmsb pull+msb image tagofconstruct-box:latestis spawned so the nextctfinds the image already staged. Opt-out via[runtime] prepull_image = false. Output goes to~/.config/construct-cli/logs/prepull.log. The detached child usesos.Executable()to re-exec the freshly installed binary (verified:installBinaryWithBackupfinishes its atomic rename before the prepull spawn at line 385, so the ordering invariant is correct). The prepull logs a warning when[runtime] prepull_image = truebut[runtime] backendis not"microvm"(the default"auto"falls through to OCI runtime and never resolves to microvm, so the silent skip is now visible). - Credential proxy design (phase 5, design only):
docs/CREDS-PROXY.mdcovers the full design: threat model, component diagram, CA lifecycle (host-generated, 1y validity, embedded in image trust store), per-provider rule format withstrip_request_authflag, keychain-backed token store (macOS Security / Linux Secret Service / permissions-protected file fallback), four-phase rollout plan withruntime.credential_proxyflag, network-mode interaction. Implementation is deferred to a follow-up; P5.2 (peer review) is the next step.
Fixed
- Daemon flock: no more false "Waiting for another construct invocation" notice on every long boot: the 250ms notice goroutine was disarmed only at
release()time instead of at acquire time, so ANY lock hold longer than 250ms — which is every real daemon boot — armed the timer for the full duration and printed the waiting notice 250ms in, uncontended or not. The notice now measures the ACQUISITION wait only (disarm via a dedicatedsync.Oncefired on acquire success or error), and the goroutine captures the stderr writer before spawn instead of reading theos.Stderrglobal late, which also fixes thego test -racedata race against tests that swapos.Stderr. - Doctor: no more duplicate "Container Runtime" line and wrong "Daemon Mode" wording under microvm: the runtime check was appended twice when
backend = "microvm"(once inside the msb branch, once at the end of the shared chain), soct sys doctorprinted "Not applicable (runtime backend = microvm)" on two consecutive lines. The Daemon Mode check then showed "Unavailable (config/runtime missing)" — misleading on microvm, whereruntimeNameis deliberately blanked: the check now reports "Not applicable (runtime backend = microvm)" and points atconstruct sys daemon statusfor the real microVM daemon state. - Doctor: stale SSH agent sockets no longer counted as "local keys": the keys check listed every file in the mounted construct home
.sshexcept a fixed non-key set, so per-bootagent.<pid>.sockproxy sockets accumulated there (60 on the dogfood host) and inflated the "Found N local keys" line. The filter is nowsshKeyNames, which skips directories,.pubcounterparts, known non-key files, and any*.sock(socket cleanup at the source is a tracked follow-up). - MicroVM daemon: skills toggle no longer silently misses the new mounts: a Sonnet peer review caught that
msbDaemonNeedsRecreatedid not consider skills mounts, so toggling skills on a running daemon left the change invisible until manual recreate. Fix: newconstruct.daemon.skills_hashlabel (see Added above). - Idle-watcher no longer races concurrent watchers or fresh
EnsureMsbDaemon: a Sonnet peer review caught thatStopMsbDaemonBestEffortdid not acquire the daemon flock, so two concurrent watchers could each independently decide "count==0" and race to stop the same daemon, AND a freshctinvocation'sEnsureMsbDaemoncould be torn down mid-flight. Fix:StopMsbDaemonBestEffortnow acquires the flock, re-checksLiveSessionCount()under the lock, then stops. A session that registered between the watcher's last tick and the stop is honored.
Docs
docs/DOGFOODING-1.16.3.mdP4.3 procedure corrected: the prepull check told the user to runct sys update, which is agent-update-inside-container and fails closed onbackend = "microvm"by design (exactly what the dogfood run hit). The deterministic path isct sys prepull(foreground pull loop, same log). Also documents thatct sys self-updateonly fires the prepull after an actual update — the "already on latest version" no-op returns before the spawn — and thatmsb image rmmatches by exact reference, not substring.docs/VMsv2.mdround 8 review provenance entry: records the Sonnet review follow-up (the two must-fix items + the nits we did not act on) so the next reviewer sees the trail.docs/VMsv2.mdP3.6 partial marker: the config template andct sys daemon statussession-count line are done; the fulldocs/CONFIGURATION.mdwrite is still pending (defer to a docs sweep).docs/VMsv2.mdround 7 entry: therequestLearnRootreturn-value split Sonnet originally flagged was traced to dead code (cleanProjectDiralready filters system roots upstream), reverted, and documented so a future maintainer does not "fix" away the no-opResolveDaemonMountsWithLearnedwrapper.
The Construct CLI 1.16.2
[1.16.2] - 2026-08-25
Fixed
- MicroVM daemon: switching projects no longer recreates the sandbox: under
backend = "microvm", moving to a different project root destroyed and recreated the daemon microVM, wiping guest state (brew and apt installs) and re-running the multi-minute first boot on every switch. The Docker backend's multi-root daemon mounts are now ported to the microVM path: with[daemon] multi_paths_enabled = true, everymount_pathsroot is mounted under/workspaces/<hash>(full-path hash, so same-named repos cannot collide) and the daemon is reused for any directory inside the set; the sandbox is recreated only when the configured mount set itself changes (microsandbox mounts are create-time only, the SDK has no hot-add). A directory outside every configured root stops with an actionable error instead of a destructive recreate. In default single-path mode, subdirectories of the mounted root now reuse the daemon (previously even a subdirectory of the same project triggered a full recreate), and stale workspace labels are detected before reuse. Workdir mapping resolves symlinks on both sides, so paths reached through macOS/tmpvs/private/tmpor symlinked checkouts no longer spuriously count as outside the mount.
Changed
[runtime] enginemerged intobackend: the two config keys encoded one decision (engine picked the OCI binary, backend only switched container vs microVM, and engine was ignored under microvm), and the literal valuedockermeant different things on each key. The singlebackendkey now takesauto(default; detects container > podman > docker),container,podman,docker, ormicrovm; pinning a binary sets the priority order and detection still falls through when the pin is unavailable. Existing configs migrate automatically on first run with an in-place, comment-preserving rewrite: a pinnedengine = "podman"becomesbackend = "podman"on the same line,engine = "auto"is dropped, and an explicitbackendpin always wins over a leftover engine value; if the file cannot be rewritten, the same rules apply in memory for that session and the legacy value can never round-trip back throughSave(). The migration write is atomic (temp file plus rename), so a crash mid-write cannot truncate config.toml. One nuance: an old explicitbackend = "docker"with no engine now pins the Docker binary instead of auto-detecting; setbackend = "auto"to restore detection (documented in docs/CONFIGURATION.md).
Docs
- MicroVM users are pointed at the daemon setup: README, docs/CONFIGURATION.md, docs/ARCHITECTURE-DESIGN.md, and the config template now state the preferred microVM workflow: enable
[daemon] multi_paths_enabledwithmount_pathscovering your project roots so the daemon sandbox is created once and reused across projects, instead of being recreated (with a full guest re-initialization) on every directory switch.
The Construct CLI 1.16.1
[1.16.1] - 2026-08-24
Fixed
- Rootless podman: agents crashed with
EACCES: permission deniedwriting under/home/construct:ct pi update --alland similar commands exec into the daemon as theconstructuser (uid 1000), but rootless podman's default user namespace maps container uid 1000 to a host subuid (e.g.100999) while container root maps to the real host user. The/home/constructbind mount is created host-side and owned by the host user, so the mappedconstructuser only had read access to it, surfacing asmkdir '/home/construct/.pi/agent/trust.json.lock'failures. The generateddocker-compose.override.ymlnow setsuserns_mode: keep-idfor rootless podman, which maps the host UID/GID directly into the container at the same numeric id instead of through the subuid range, soconstruct(uid 1000) aliases the host user and can write the mount it already owns.entrypoint.sh's root-phase ownership fix carried the opposite assumption (container root aliases the host user, so stay root and skip the chown); it now reads a newCONSTRUCT_USERNS_KEEPIDsignal to skip that fallback under keep-id and take the normal numeric-uid chown-then-drop-privileges path instead. The override cache key gained a version marker so every existingdocker-compose.override.ymlregenerates with the fix on next run; a daemon container already running under the old mapping keeps it until recreated (construct sys doctor --fix, which always stops and recreates the daemon), sinceuserns_modeonly takes effect at container creation.
The Construct CLI 1.16.0
[1.16.0] - 2026-08-23
Added
- MicroVM hardware isolation engine (
backend = "microvm"): Construct now supports microVM hardware isolation powered by microsandbox as an opt-in runtime backend alongside OCI containers. Each sandbox runs within an isolated Linux guest kernel managed by hardware hypervisors (Apple Hypervisor.framework on macOS, KVM on Linux). The runtime layer standardizes lifecycle, command execution, streaming stdio, interactive PTY sessions, and image inspection through a modularBackendinterface (internal/runtime/backend.go). - Full guest-to-host bridge subsystem for microVMs: All Construct bridges communicate across the microVM boundary via
host.microsandbox.internal: token-authenticated clipboard text and image pasting, bidirectional host-exec path mapping, SSH agent proxying (with socket listeners placed on guest tmpfs to bypass VirtioFS socket creation constraints), Herdr status reporting, and loopback TCP relays (127.0.0.1:<port>->host.microsandbox.internal:<port>) for headless browser access to host development sites. - Resource sizing and persistent daemon management: Sandboxes configure 4 vCPUs and 4096 MiB RAM (
CPUs: 4, MemoryMiB: 4096) by default, preventing memory starvation or Linux OOM termination on heavy JS/TS agent heaps (Claude Code, Pi extensions, Codex, OpenCode). Sandboxes dynamically recreate when switching across host project roots, persist toolchains and package installations across stop/start cycles on the sandbox root disk, and validate backend readiness fail-closed without silent fallback. - MicroVM health diagnostics:
construct sys doctorincludes dedicated checks for the microVM backend (binary detection, daemon reachability, image state, and hypervisor virtualization support). rtk(Rust Token Killer) joins the default brew packages: token counting and context budgeting inside sandboxes, present on every fresh install.
Fixed
- podman-compose parse failure on SELinux-labeled read-only mounts: the generated override emitted the global gitignore bind as
src:dst:ro:z; podman-compose's short-mount grammar accepts at most three colon-separated fields, so every setup on a podman host died withcould not parse mountbefore the container started. Mounts that already carry a mode flag now append the SELinux label as a comma-joined option (ro,z), which docker compose also accepts, and the override cache key gained amount_mode_syntaxfield so existingdocker-compose.override.ymlfiles regenerate instead of keeping the unparseable line. - Bun bootstrap failed on fresh containers (
unzip is required to install bun): the user-package installer reaches the Bun step before any brew formula is populated, and the Debian base image ships no unzip.unzipjoins the base image apt layer so the installer path is self-sufficient. - Config ownership doctor blocked scripted and agent sessions: rootless podman's user namespace periodically rewrites files under
~/.config/construct-clithrough the:zmounts, leaving them owned by a subordinate UID that blocks host-side writes. The doctor confirmed with a prompt that reads stdin (hung or consumed the piped stream in ssh batches and agent runs), and the migration fix path went straight to sudo, which cannot ask for a password without a terminal. Confirm prompts now gate onui.StdinIsTerminal(): non-interactive sessions run the rootless fix directly,runOwnershipFixtriespodman unshare chown -R 0:0before the sudo fallback, and sudo only inherits stdin on a real terminal.
The Construct CLI 1.15.1
[1.15.1] - 2026-08-20
Added
construct sys shims: real PATH executables that route agents through the sandbox, for tools that spawn agent binaries directly: shell aliases only exist inside an interactive shell, so orchestrators, IDE extensions, and CI wrappers that resolve an agent binary on PATH or exec it without a shell (Paseo and similar harnesses spawningpi --mode rpc) never saw the aliases and ran the bare host binary.construct sys shims --install(default dir~/.local/bin) writes two executables per supported agent:<slug>execsconstruct <slug>with stdin/stdout passed through unchanged (JSONL RPC streams stay clean), andns-<slug>execs the real host binary directly, non-sandboxed (the ns- shell functions from the old alias system, now as files; the real binary is resolved on PATH while skipping the shim dir so it can never resolve to our own shim). Refuses to overwrite files it did not write (--forceoverrides), warns when the shim dir is off-PATH or another binary wins resolution, and--uninstallremoves only files carrying our marker.--remove-aliasesperforms a standalone cleanup of the legacy managed shell alias block for upgraders who do not want shims.- Host path arguments are staged into the sandbox for harness-driven runs: orchestrators pass absolute host paths as flag values (pi
--extensionand--mcp-configpoint at temp bridge files under/var/folders,--sessionat the host~/.pistore), and those paths do not exist inside the container (the sandbox home is the bind-mounted construct home; macOS Docker Desktop does not share/var/folders). Flagged values for known agents that resolve to existing host files under trusted roots (temp trees, the agent's host config dir, the caller's cwd) are copied to<construct home>/.construct-staging/<run-id>/(0700, 8 MB per file, 16 files per run) and rewritten to their/home/construct/.construct-staging/...paths, inengine.Prepareso the daemon exec, compose run, and msb paths all inherit it. Session files register a copy-back on Teardown so sandbox progress reaches the original host store. Values outside the allowlist are left untouched. Verified live with the exact Paseo argv: bridge extension loaded in-sandbox, sandbox-born session resumed, clean JSONL on stdout.
Changed
- The
construct sys aliasessystem is removed: shell aliases were invisible to non-shell callers, which is the gap shims close.sys shims --installmigrates existing setups by removing the managed# construct-cli aliases start/endblock from the shell rc (timestamped backup first; hand-written aliases and functions are never touched). For muscle memory and older instructions,construct sys aliases --uninstallstill works (deprecation note, removes the block, exit 0) while any other legacy aliases flag prints the replacement commands and exits 1. - All run-path status output now goes to stderr, keeping stdout reserved for the agent's own output: banners such as
Running in Construct daemon: [...], daemon startup, SSH/Herdr proxy notices, rebuild hints, migration chatter, first-run initialization, and image-build progress printed to stdout, which corrupts line-delimited JSON streams consumed by harnesses and other non-interactive callers. Newui.Info/InfoLn/InfoFhelpers carry these messages oninternal/agent(engine, runner, msb),internal/migration,internal/runtime,internal/env,internal/config, andinternal/network; interactive output is visually unchanged because terminals merge both streams. The interactive attach prompt and explicit CLI output (construct agents, help) intentionally remain on stdout. Verified warm:pi --mode rpcthrough an installed shim returns a pure JSONL stream with zero non-JSON stdout lines; cold-start output (first-run init, image build) also lands on stderr.
The Construct CLI 1.15.0
[1.15.0] - 2026-08-20
Added
construct sys shims: real PATH executables that route agents through the sandbox, for tools that spawn agent binaries directly: shell aliases only exist inside an interactive shell, so orchestrators, IDE extensions, and CI wrappers that resolve an agent binary on PATH or exec it without a shell (Paseo and similar harnesses spawningpi --mode rpc) never saw the aliases and ran the bare host binary.construct sys shims --install(default dir~/.local/bin) writes two executables per supported agent:<slug>execsconstruct <slug>with stdin/stdout passed through unchanged (JSONL RPC streams stay clean), andns-<slug>execs the real host binary directly, non-sandboxed (the ns- shell functions from the old alias system, now as files; the real binary is resolved on PATH while skipping the shim dir so it can never resolve to our own shim). Refuses to overwrite files it did not write (--forceoverrides), warns when the shim dir is off-PATH or another binary wins resolution, and--uninstallremoves only files carrying our marker.--remove-aliasesperforms a standalone cleanup of the legacy managed shell alias block for upgraders who do not want shims.
Changed
- The
construct sys aliasessystem is removed: shell aliases were invisible to non-shell callers, which is the gap shims close.sys shims --installmigrates existing setups by removing the managed# construct-cli aliases start/endblock from the shell rc (timestamped backup first; hand-written aliases and functions are never touched). For muscle memory and older instructions,construct sys aliases --uninstallstill works (deprecation note, removes the block, exit 0) while any other legacy aliases flag prints the replacement commands and exits 1. - All run-path status output now goes to stderr, keeping stdout reserved for the agent's own output: banners such as
Running in Construct daemon: [...], daemon startup, SSH/Herdr proxy notices, rebuild hints, migration chatter, first-run initialization, and image-build progress printed to stdout, which corrupts line-delimited JSON streams consumed by harnesses and other non-interactive callers. Newui.Info/InfoLn/InfoFhelpers carry these messages oninternal/agent(engine, runner, msb),internal/migration,internal/runtime,internal/env,internal/config, andinternal/network; interactive output is visually unchanged because terminals merge both streams. The interactive attach prompt and explicit CLI output (construct agents, help) intentionally remain on stdout. Verified warm:pi --mode rpcthrough an installed shim returns a pure JSONL stream with zero non-JSON stdout lines; cold-start output (first-run init, image build) also lands on stderr.
The Construct CLI 1.14.2
[1.14.2] - 2026-07-30
Fixed
- Host exec cwd propagation now covers daemon
/workspacesmounts, not just the project mount: 1.14.1 translated the agent's container cwd only against the primary project mount (/projects/<name>), so an agent whose cwd sat under a daemon multi-path mount (/workspaces/<sha256[:8]>, one perdaemon.mount_pathsentry) could not be translated andwicket wpfell back to the daemon cwd, failing withno services found.StartServernow takes a list ofPathMap{Container,Host};internal/agent/engine.gopasses the project mount plus everyruntime.ResolveDaemonMountsmount, andresolveHostCwdtries each map while still rejecting anything that escapes a mount's host root. Empty or unknown cwd still falls back to inheriting the daemon cwd. Backward compatible; the home mount (/home/construct) remains untranslated. Requiresconstruct buildto bake the updated shim. Documented indocs/HOST-EXEC.md.
The Construct CLI 1.14.1
[1.14.1] - 2026-07-30
Added
- Host exec bridge now propagates the agent's working directory to host binaries: when an agent invokes an allowlisted host binary (
host_binaries, e.g.wicket) from inside the sandbox, the bridge previously ran it in the daemon's cwd. So cwd-aware CLIs likewicket wp(which detect the Docker Compose project by walking up from cwd for a compose file, with no--projectflag) resolved the wrong project and failed withno services found. The shim (internal/templates/construct-host-exec) now sends the container$PWDas acwdfield on the/execpayload;internal/hostexec/server.gotranslates it to the matching host path via the project mount (${PWD}:${CONSTRUCT_PROJECT_PATH}), validates the result stays at or under the host project root (rejecting../escapes and arbitrary host dirs like/etc), and sets it as the child's working directory. Empty, unset, or out-of-mount cwd falls back to inheriting the daemon cwd (the previous behavior), so the change is backward compatible.internal/agent/engine.gopassesruntime.GetProjectMountPath()(container root) ande.cwd(host root) toStartServer. After this, an agent cancdinto a client project and runwicket wp/wicket dockerand have them target that project on the host. Security: cwd is constrained to the project mount; anything outside the host project root is rejected, which adds nothing the agent could not already do (it can alreadycdthere and the binary already runs as the host user). v1 limitation: only the project mount is translated, not the home mount (/home/construct); a cwd there falls back to inheriting. Requiresconstruct buildto bake the updated shim into the image. Documented indocs/HOST-EXEC.md.
The Construct CLI 1.14.0
[1.14.0] - 2026-07-28
Added
- Host loopback forwarding lets headless browsers reach host dev sites: agent-browser's Chromium runs inside the sandbox, but Chromium hardcodes
localhostand*.localhostto127.0.0.1(RFC 6761), bypassing/etc/hosts, DNS,dnsmasq, and--host-resolver-rules. So a headless browser could not reach host dev servers likehttp://hyperpress.localhost, even though non-browser tools (curl, git, MCP) reached them fine. The fix is blind TCP relays on the container's127.0.0.1that forward tohost.docker.internal, launched byentrypoint.sh(socat, next to the SSH bridge). Blind relay preserves the HTTP Host header and TLS SNI, so host vhost routers (valet, Hyperpress) and certs see the real hostname. Configured via[sandbox] host_loopback_ports(list of ints, default[80, 443], same port both sides). EmittingCONSTRUCT_LOOPBACK_PORTSintodocker-compose.override.ymlalso addscap_add: NET_BIND_SERVICE(consolidated with strict-modeNET_ADMINinto onecap_add:block) so the non-root construct user's socat can bind privileged ports. The port list is hash-tracked inoverrideInputs, so changing it regenerates the override; an empty list disables the feature and drops the cap. Platform caveat: on Linux the host-gateway is the bridge IP, so host services must bind0.0.0.0/bridge (not127.0.0.1-only) to be reachable; macOS host-gateway already routes to host127.0.0.1. - Terminal identity markers now forward into the sandbox:
KITTY_WINDOW_ID,GHOSTTY_RESOURCES_DIR, andTERM_PROGRAMare passed into the container as-eflags on both launch paths (directcompose runand the daemoncompose run -d, sodocker execsessions inherit them) so in-container TUIs and pi extensions can detect the outer terminal and do the right thing (kitty-graphics inline image rendering, etc.).TERMitself is intentionally NOT forwarded by default to avoid terminfo mismatches (forwardingxterm-kittyinto a container without kitty-terminfo breaks ncurses apps like less/vim/btop); the identity vars are enough for detection. Users who needTERMcan add it toenv_passthroughand installncurses-term/kitty-terminfoin the image.
Fixed
construct buildno longer fails on the socat file cap: the Dockerfile ransetcap cap_net_bind_service+ep /usr/bin/socatat build time, but BuildKit's default sandbox blocks file-capability writes duringdocker buildand aborts withInvalid file 'setcap' for capability operation(granting it needs thesecurity.insecureentitlement, whichdocker compose builddoes not enable by default). The file cap is now applied at runtime:entrypoint.shruns the samesetcapas root in its startup block, before thegosudrop to the construct user. It is idempotent and best-effort, and the cap persists on the overlay for the container's lifetime, so the non-root socat still binds80/443via the file cap plus theNET_BIND_SERVICEbounding set (both still required).libcap2-bin(which provides thesetcapbinary) stays installed in the image.