Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Releases: EstebanForge/construct-cli

The Construct CLI 1.16.4

Choose a tag to compare

@github-actions github-actions released this 27 Aug 12:15

[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 msb CLI could be newer. A newer CLI migrates ~/.microsandbox/db forward and the embedded engine then fails every daemon create with database 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 tag does 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 -i also imports archives under the localhost/ 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, and ct 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 over logs/*.log matched nothing even on a healthy install. msbLogBoot now 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.md section 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

Choose a tag to compare

@github-actions github-actions released this 27 Aug 01:43

[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>/skills for ten supported agents (agy, claude, amp, qwen, copilot, crush, droid, goose, kilocode, cline). Source resolution precedence: $CONSTRUCT_SKILLS_SOURCE env 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 = false when an agent must author skills. Docker compose uses :ro (or :ro,z on Linux SELinux); microVM msb.Mount.Bind honors the same flag. Fails closed (no mount, no error) when the source does not resolve. Managed entirely by construct-cli; manage.sh no 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 by BuildMsbRunSpec whenever skills are enabled, and msbDaemonNeedsRecreate checks 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 in msbSandboxMounts + MsbPathMaps) is still pending; the hash surface is ready for it.
  • Boot telemetry (msb-boot: log line): EnsureMsbDaemon now emits a stable msb-boot: line at every return path with the outcome (cold | recreate | warm | reconnect), elapsed seconds, mount count, and (for recreate) the reason. msbBootClock is injectable so tests run deterministic without sleeping. Output goes to stderr (run-path rule respected). Numbers land in docs/VMsv2.md section 10 once the dogfood week collects medians; P6 (snapshot fork) is gated on those numbers.
  • Widened daemon flock (phase 1): a blocking syscall.Flock on ~/.config/construct-cli/daemon.lock (mode 0600) is acquired at the top of EnsureMsbDaemon via defer releaseLock(). The critical section now wraps read state, decide, write state, and the recreate/boot itself so concurrent ct invocations 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 doctor surfaces daemon lock: free/held (per-request probe, intentionally not transactional with the actual lock).
  • Learned roots store (phase 2 data layer): roots.json is a versioned, atomic-write JSON store at ~/.config/construct-cli/roots.json with LRU eviction ([daemon] max_learned_roots, default 8). requestLearnRoot enforces the workspace guard, prompts via gum when interactive, denies with an actionable message when non-interactive. The full EnsureMsbDaemon wire-in (P2.2) is pending; the helper is shipped with nolint:unused and 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) and construct sys daemon roots forget <path> (refuses configured daemon.mount_paths entries). Pinned paths and forgotten paths are shown side by side; the manage.sh symlink/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 live ct) is read+written only inside the daemon flock critical section. execViaMsbDaemon registers the current PID; Teardown unregisters and, on the LAST unregister, spawns a detached construct sys daemon idle-watch process (true daemon via Setsid, 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 new ct invocation that registers a session during the sleep stands the watcher down. The watcher acquires the flock before stopping (round 8 fix) so a fresh EnsureMsbDaemon cannot 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 detached msb pull + msb image tag of construct-box:latest is spawned so the next ct finds the image already staged. Opt-out via [runtime] prepull_image = false. Output goes to ~/.config/construct-cli/logs/prepull.log. The detached child uses os.Executable() to re-exec the freshly installed binary (verified: installBinaryWithBackup finishes 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 = true but [runtime] backend is 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.md covers the full design: threat model, component diagram, CA lifecycle (host-generated, 1y validity, embedded in image trust store), per-provider rule format with strip_request_auth flag, keychain-backed token store (macOS Security / Linux Secret Service / permissions-protected file fallback), four-phase rollout plan with runtime.credential_proxy flag, 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 dedicated sync.Once fired on acquire success or error), and the goroutine captures the stderr writer before spawn instead of reading the os.Stderr global late, which also fixes the go test -race data race against tests that swap os.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), so ct sys doctor printed "Not applicable (runtime backend = microvm)" on two consecutive lines. The Daemon Mode check then showed "Unavailable (config/runtime missing)" — misleading on microvm, where runtimeName is deliberately blanked: the check now reports "Not applicable (runtime backend = microvm)" and points at construct sys daemon status for 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 .ssh except a fixed non-key set, so per-boot agent.<pid>.sock proxy sockets accumulated there (60 on the dogfood host) and inflated the "Found N local keys" line. The filter is now sshKeyNames, which skips directories, .pub counterparts, 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 msbDaemonNeedsRecreate did not consider skills mounts, so toggling skills on a running daemon left the change invisible until manual recreate. Fix: new construct.daemon.skills_hash label (see Added above).
  • Idle-watcher no longer races concurrent watchers or fresh EnsureMsbDaemon: a Sonnet peer review caught that StopMsbDaemonBestEffort did not acquire the daemon flock, so two concurrent watchers could each independently decide "count==0" and race to stop the same daemon, AND a fresh ct invocation's EnsureMsbDaemon could be torn down mid-flight. Fix: StopMsbDaemonBestEffort now acquires the flock, re-checks LiveSessionCount() 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.md P4.3 procedure corrected: the prepull check told the user to run ct sys update, which is agent-update-inside-container and fails closed on backend = "microvm" by design (exactly what the dogfood run hit). The deterministic path is ct sys prepull (foreground pull loop, same log). Also documents that ct sys self-update only fires the prepull after an actual update — the "already on latest version" no-op returns before the spawn — and that msb image rm matches by exact reference, not substring.
  • docs/VMsv2.md round 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.md P3.6 partial marker: the config template and ct sys daemon status session-count line are done; the full docs/CONFIGURATION.md write is still pending (defer to a docs sweep).
  • docs/VMsv2.md round 7 entry: the requestLearnRoot return-value split Sonnet originally flagged was traced to dead code (cleanProjectDir already filters system roots upstream), reverted, and documented so a future maintainer does not "fix" away the no-op ResolveDaemonMountsWithLearned wrapper.

The Construct CLI 1.16.2

Choose a tag to compare

@github-actions github-actions released this 25 Aug 14:06

[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, every mount_paths root 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 /tmp vs /private/tmp or symlinked checkouts no longer spuriously count as outside the mount.

Changed

  • [runtime] engine merged into backend: 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 value docker meant different things on each key. The single backend key now takes auto (default; detects container > podman > docker), container, podman, docker, or microvm; 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 pinned engine = "podman" becomes backend = "podman" on the same line, engine = "auto" is dropped, and an explicit backend pin 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 through Save(). The migration write is atomic (temp file plus rename), so a crash mid-write cannot truncate config.toml. One nuance: an old explicit backend = "docker" with no engine now pins the Docker binary instead of auto-detecting; set backend = "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_enabled with mount_paths covering 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

Choose a tag to compare

@github-actions github-actions released this 24 Aug 14:29

[1.16.1] - 2026-08-24

Fixed

  • Rootless podman: agents crashed with EACCES: permission denied writing under /home/construct: ct pi update --all and similar commands exec into the daemon as the construct user (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/construct bind mount is created host-side and owned by the host user, so the mapped construct user only had read access to it, surfacing as mkdir '/home/construct/.pi/agent/trust.json.lock' failures. The generated docker-compose.override.yml now sets userns_mode: keep-id for rootless podman, which maps the host UID/GID directly into the container at the same numeric id instead of through the subuid range, so construct (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 new CONSTRUCT_USERNS_KEEPID signal 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 existing docker-compose.override.yml regenerates 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), since userns_mode only takes effect at container creation.

The Construct CLI 1.16.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 02:42

[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 modular Backend interface (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 doctor includes 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 with could not parse mount before 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 a mount_mode_syntax field so existing docker-compose.override.yml files 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. unzip joins 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-cli through the :z mounts, 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 on ui.StdinIsTerminal(): non-interactive sessions run the rootless fix directly, runOwnershipFix tries podman unshare chown -R 0:0 before the sudo fallback, and sudo only inherits stdin on a real terminal.

The Construct CLI 1.15.1

Choose a tag to compare

@github-actions github-actions released this 21 Aug 00:57

[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 spawning pi --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> execs construct <slug> with stdin/stdout passed through unchanged (JSONL RPC streams stay clean), and ns-<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 (--force overrides), warns when the shim dir is off-PATH or another binary wins resolution, and --uninstall removes only files carrying our marker. --remove-aliases performs 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 --extension and --mcp-config point at temp bridge files under /var/folders, --session at the host ~/.pi store), 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, in engine.Prepare so 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 aliases system is removed: shell aliases were invisible to non-shell callers, which is the gap shims close. sys shims --install migrates existing setups by removing the managed # construct-cli aliases start/end block from the shell rc (timestamped backup first; hand-written aliases and functions are never touched). For muscle memory and older instructions, construct sys aliases --uninstall still 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. New ui.Info/InfoLn/InfoF helpers carry these messages on internal/agent (engine, runner, msb), internal/migration, internal/runtime, internal/env, internal/config, and internal/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 rpc through 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

Choose a tag to compare

@github-actions github-actions released this 20 Aug 23:23

[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 spawning pi --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> execs construct <slug> with stdin/stdout passed through unchanged (JSONL RPC streams stay clean), and ns-<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 (--force overrides), warns when the shim dir is off-PATH or another binary wins resolution, and --uninstall removes only files carrying our marker. --remove-aliases performs a standalone cleanup of the legacy managed shell alias block for upgraders who do not want shims.

Changed

  • The construct sys aliases system is removed: shell aliases were invisible to non-shell callers, which is the gap shims close. sys shims --install migrates existing setups by removing the managed # construct-cli aliases start/end block from the shell rc (timestamped backup first; hand-written aliases and functions are never touched). For muscle memory and older instructions, construct sys aliases --uninstall still 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. New ui.Info/InfoLn/InfoF helpers carry these messages on internal/agent (engine, runner, msb), internal/migration, internal/runtime, internal/env, internal/config, and internal/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 rpc through 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

Choose a tag to compare

@github-actions github-actions released this 30 Jul 16:43

[1.14.2] - 2026-07-30

Fixed

  • Host exec cwd propagation now covers daemon /workspaces mounts, 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 per daemon.mount_paths entry) could not be translated and wicket wp fell back to the daemon cwd, failing with no services found. StartServer now takes a list of PathMap{Container,Host}; internal/agent/engine.go passes the project mount plus every runtime.ResolveDaemonMounts mount, and resolveHostCwd tries 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. Requires construct build to bake the updated shim. Documented in docs/HOST-EXEC.md.

The Construct CLI 1.14.1

Choose a tag to compare

@github-actions github-actions released this 30 Jul 15:16

[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 like wicket wp (which detect the Docker Compose project by walking up from cwd for a compose file, with no --project flag) resolved the wrong project and failed with no services found. The shim (internal/templates/construct-host-exec) now sends the container $PWD as a cwd field on the /exec payload; internal/hostexec/server.go translates 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.go passes runtime.GetProjectMountPath() (container root) and e.cwd (host root) to StartServer. After this, an agent can cd into a client project and run wicket wp / wicket docker and 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 already cd there 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. Requires construct build to bake the updated shim into the image. Documented in docs/HOST-EXEC.md.

The Construct CLI 1.14.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 01:33

[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 localhost and *.localhost to 127.0.0.1 (RFC 6761), bypassing /etc/hosts, DNS, dnsmasq, and --host-resolver-rules. So a headless browser could not reach host dev servers like http://hyperpress.localhost, even though non-browser tools (curl, git, MCP) reached them fine. The fix is blind TCP relays on the container's 127.0.0.1 that forward to host.docker.internal, launched by entrypoint.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). Emitting CONSTRUCT_LOOPBACK_PORTS into docker-compose.override.yml also adds cap_add: NET_BIND_SERVICE (consolidated with strict-mode NET_ADMIN into one cap_add: block) so the non-root construct user's socat can bind privileged ports. The port list is hash-tracked in overrideInputs, 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 bind 0.0.0.0/bridge (not 127.0.0.1-only) to be reachable; macOS host-gateway already routes to host 127.0.0.1.
  • Terminal identity markers now forward into the sandbox: KITTY_WINDOW_ID, GHOSTTY_RESOURCES_DIR, and TERM_PROGRAM are passed into the container as -e flags on both launch paths (direct compose run and the daemon compose run -d, so docker exec sessions 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.). TERM itself is intentionally NOT forwarded by default to avoid terminfo mismatches (forwarding xterm-kitty into a container without kitty-terminfo breaks ncurses apps like less/vim/btop); the identity vars are enough for detection. Users who need TERM can add it to env_passthrough and install ncurses-term/kitty-terminfo in the image.

Fixed

  • construct build no longer fails on the socat file cap: the Dockerfile ran setcap cap_net_bind_service+ep /usr/bin/socat at build time, but BuildKit's default sandbox blocks file-capability writes during docker build and aborts with Invalid file 'setcap' for capability operation (granting it needs the security.insecure entitlement, which docker compose build does not enable by default). The file cap is now applied at runtime: entrypoint.sh runs the same setcap as root in its startup block, before the gosu drop 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 binds 80/443 via the file cap plus the NET_BIND_SERVICE bounding set (both still required). libcap2-bin (which provides the setcap binary) stays installed in the image.