From 0249ed597da8e1fed2c9f056989c52a188c56b8f Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 14:52:01 -0400 Subject: [PATCH 1/6] ^R Extract Go/Colima host-utility wrappers into scripts/lib/launcher/go-hostutil.sh (byte-identical move, shellcheck/shfmt/bash-n green and module body proven equal via diff/md5 7c6ccafdc006e9b642f40d1e0de603a2; launcher is user-visible but behavior is unchanged) (risk: touches the go_hostutil/go_colimautil primitives the launcher uses to run workcell-hostutil and workcell-colimautil on the host via go run, plus the exit-code recovery and the go_hostutil_publish_pr credential env-var allowlist, so a subtle change could weaken the runtime boundary or leak the sanitised environment; case: pure contiguous move of go_hostutil/run_go_hostutil_preserve_exit/go_hostutil_publish_pr/go_colimautil with zero logic change, dependencies ensure_go_run_env + run_clean_host_command_in_dir + ROOT_DIR/HOST_GO_BIN already sourced or assigned before the new module, first call site well after the source line; the only workcell edit beyond the deletion is a source line, the shell_files allowlist additions, and an SC2034 disable on HOST_GO_BIN whose sole use now lives in the sourced module) Co-Authored-By: Claude Opus 4.8 --- scripts/dev-quick-check.sh | 1 + scripts/lib/launcher/go-hostutil.sh | 100 ++++++++++++++++++++++++++++ scripts/validate-repo.sh | 1 + scripts/workcell | 80 ++-------------------- 4 files changed, 107 insertions(+), 75 deletions(-) create mode 100755 scripts/lib/launcher/go-hostutil.sh diff --git a/scripts/dev-quick-check.sh b/scripts/dev-quick-check.sh index 6c8edc82..7f821140 100755 --- a/scripts/dev-quick-check.sh +++ b/scripts/dev-quick-check.sh @@ -52,6 +52,7 @@ shell_files=( "${ROOT_DIR}/scripts/lib/go-run-env.sh" "${ROOT_DIR}/scripts/lib/launcher/host-detect.sh" "${ROOT_DIR}/scripts/lib/launcher/host-exec.sh" + "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" "${ROOT_DIR}/scripts/lib/manage_injection_policy" "${ROOT_DIR}/scripts/lib/pty_transcript" "${ROOT_DIR}/scripts/lib/render_injection_bundle" diff --git a/scripts/lib/launcher/go-hostutil.sh b/scripts/lib/launcher/go-hostutil.sh new file mode 100755 index 00000000..96965f62 --- /dev/null +++ b/scripts/lib/launcher/go-hostutil.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env -S BASH_ENV= ENV= bash +# shellcheck shell=bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Omkhar Arasaratnam +# +# scripts/lib/launcher/go-hostutil.sh — Go/Colima host-utility wrapper +# module extracted from scripts/workcell as the next increment of the +# launcher decomposition (roadmap item D4). These helpers invoke the +# workcell-hostutil and workcell-colimautil Go programs on the host via +# `go run`, always routed through run_clean_host_command_in_dir so the +# child executes from ${ROOT_DIR} under the sanitised host environment +# (env -i with a pinned PATH/HOME and C locale) provided by +# scripts/lib/launcher/host-exec.sh. They depend only on +# ensure_go_run_env plus the GOPATH/GOMODCACHE/GOCACHE it exports +# (scripts/lib/go-run-env.sh), run_clean_host_command_in_dir +# (scripts/lib/launcher/host-exec.sh), and the readonly ROOT_DIR and +# HOST_GO_BIN globals set in scripts/workcell — all sourced or assigned +# before the first wrapper call — so they are a self-contained, +# behaviour-preserving unit. run_go_hostutil_preserve_exit additionally +# recovers the Go child's real exit code from its `exit status N` stderr +# trailer. go_hostutil_publish_pr forwards an explicit allowlist of +# terminal/GnuPG/SSH/XDG/GitHub environment variables (read at call time) +# so host-side PR publication can reach the operator's credentials. See +# docs/launcher-contract.md for the module contract. + +go_hostutil() { + ensure_go_run_env + run_clean_host_command_in_dir "${ROOT_DIR}" env \ + GOPATH="${GOPATH}" \ + GOMODCACHE="${GOMODCACHE}" \ + GOCACHE="${GOCACHE}" \ + "${HOST_GO_BIN}" run ./cmd/workcell-hostutil "$@" +} + +run_go_hostutil_preserve_exit() { + local stderr_file="" + local stderr_capture="" + local rc=0 + + stderr_file="$(mktemp "${TMPDIR:-/tmp}/workcell-hostutil-stderr.XXXXXX")" + trap 'rm -f "${stderr_file}"' RETURN + go_hostutil "$@" 2>"${stderr_file}" || rc=$? + stderr_capture="$(cat "${stderr_file}")" + rm -f "${stderr_file}" + trap - RETURN + + if [[ "${stderr_capture}" =~ (^|$'\n')exit\ status\ ([0-9]+)$ ]]; then + if [[ ${rc} -eq 1 ]]; then + rc="${BASH_REMATCH[2]}" + fi + if [[ -z "${BASH_REMATCH[1]}" ]]; then + stderr_capture="" + else + stderr_capture="${stderr_capture%$'\n'exit status [0-9]*}" + fi + fi + if [[ -n "${stderr_capture}" ]]; then + printf '%s\n' "${stderr_capture}" >&2 + fi + return "${rc}" +} + +go_hostutil_publish_pr() { + ensure_go_run_env + local -a env_args=( + "GOPATH=${GOPATH}" + "GOMODCACHE=${GOMODCACHE}" + "GOCACHE=${GOCACHE}" + ) + + [[ -n "${TERM:-}" ]] && env_args+=("TERM=${TERM}") + [[ -n "${GPG_TTY:-}" ]] && env_args+=("GPG_TTY=${GPG_TTY}") + [[ -n "${GNUPGHOME:-}" ]] && env_args+=("GNUPGHOME=${GNUPGHOME}") + [[ -n "${SSH_AUTH_SOCK:-}" ]] && env_args+=("SSH_AUTH_SOCK=${SSH_AUTH_SOCK}") + [[ -n "${SSH_AGENT_PID:-}" ]] && env_args+=("SSH_AGENT_PID=${SSH_AGENT_PID}") + [[ -n "${SSH_ASKPASS:-}" ]] && env_args+=("SSH_ASKPASS=${SSH_ASKPASS}") + [[ -n "${GIT_ASKPASS:-}" ]] && env_args+=("GIT_ASKPASS=${GIT_ASKPASS}") + [[ -n "${XDG_CONFIG_HOME:-}" ]] && env_args+=("XDG_CONFIG_HOME=${XDG_CONFIG_HOME}") + [[ -n "${XDG_STATE_HOME:-}" ]] && env_args+=("XDG_STATE_HOME=${XDG_STATE_HOME}") + [[ -n "${XDG_CACHE_HOME:-}" ]] && env_args+=("XDG_CACHE_HOME=${XDG_CACHE_HOME}") + [[ -n "${XDG_DATA_HOME:-}" ]] && env_args+=("XDG_DATA_HOME=${XDG_DATA_HOME}") + [[ -n "${XDG_RUNTIME_DIR:-}" ]] && env_args+=("XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}") + [[ -n "${GH_TOKEN:-}" ]] && env_args+=("GH_TOKEN=${GH_TOKEN}") + [[ -n "${GITHUB_TOKEN:-}" ]] && env_args+=("GITHUB_TOKEN=${GITHUB_TOKEN}") + [[ -n "${GH_HOST:-}" ]] && env_args+=("GH_HOST=${GH_HOST}") + [[ -n "${GH_CONFIG_DIR:-}" ]] && env_args+=("GH_CONFIG_DIR=${GH_CONFIG_DIR}") + + run_clean_host_command_in_dir "${ROOT_DIR}" env \ + "${env_args[@]}" \ + "${HOST_GO_BIN}" run ./cmd/workcell-hostutil "$@" +} + +go_colimautil() { + ensure_go_run_env + run_clean_host_command_in_dir "${ROOT_DIR}" env \ + GOPATH="${GOPATH}" \ + GOMODCACHE="${GOMODCACHE}" \ + GOCACHE="${GOCACHE}" \ + "${HOST_GO_BIN}" run ./cmd/workcell-colimautil "$@" +} diff --git a/scripts/validate-repo.sh b/scripts/validate-repo.sh index 3929eb25..e2fb6d22 100755 --- a/scripts/validate-repo.sh +++ b/scripts/validate-repo.sh @@ -157,6 +157,7 @@ shell_files=( "${ROOT_DIR}/scripts/lib/go-run-env.sh" "${ROOT_DIR}/scripts/lib/launcher/host-detect.sh" "${ROOT_DIR}/scripts/lib/launcher/host-exec.sh" + "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" "${ROOT_DIR}/scripts/lib/trusted-entrypoint.sh" "${ROOT_DIR}/scripts/lib/manage_injection_policy" "${ROOT_DIR}/scripts/lib/pty_transcript" diff --git a/scripts/workcell b/scripts/workcell index 745fa925..622cdb21 100755 --- a/scripts/workcell +++ b/scripts/workcell @@ -68,6 +68,11 @@ source "${ROOT_DIR}/scripts/lib/sessionctl-shim.sh" source "${ROOT_DIR}/scripts/lib/launcher/host-detect.sh" # shellcheck source=/dev/null source "${ROOT_DIR}/scripts/lib/launcher/host-exec.sh" +# shellcheck source=/dev/null +source "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" +# HOST_GO_BIN is consumed by the go-hostutil.sh wrappers sourced above; the +# source=/dev/null directive hides that cross-file use from ShellCheck. +# shellcheck disable=SC2034 HOST_GO_BIN="$(resolve_fixed_host_tool go /opt/homebrew/bin/go /usr/local/go/bin/go /usr/local/bin/go /usr/bin/go)" REAL_HOME="$(resolve_workcell_real_home)" case "$(uname -s 2>/dev/null || true)" in @@ -78,81 +83,6 @@ case "$(uname -s 2>/dev/null || true)" in export WORKCELL_GO_CACHE_ROOT="${XDG_CACHE_HOME:-${REAL_HOME}/.cache}/workcell/go" ;; esac -go_hostutil() { - ensure_go_run_env - run_clean_host_command_in_dir "${ROOT_DIR}" env \ - GOPATH="${GOPATH}" \ - GOMODCACHE="${GOMODCACHE}" \ - GOCACHE="${GOCACHE}" \ - "${HOST_GO_BIN}" run ./cmd/workcell-hostutil "$@" -} - -run_go_hostutil_preserve_exit() { - local stderr_file="" - local stderr_capture="" - local rc=0 - - stderr_file="$(mktemp "${TMPDIR:-/tmp}/workcell-hostutil-stderr.XXXXXX")" - trap 'rm -f "${stderr_file}"' RETURN - go_hostutil "$@" 2>"${stderr_file}" || rc=$? - stderr_capture="$(cat "${stderr_file}")" - rm -f "${stderr_file}" - trap - RETURN - - if [[ "${stderr_capture}" =~ (^|$'\n')exit\ status\ ([0-9]+)$ ]]; then - if [[ ${rc} -eq 1 ]]; then - rc="${BASH_REMATCH[2]}" - fi - if [[ -z "${BASH_REMATCH[1]}" ]]; then - stderr_capture="" - else - stderr_capture="${stderr_capture%$'\n'exit status [0-9]*}" - fi - fi - if [[ -n "${stderr_capture}" ]]; then - printf '%s\n' "${stderr_capture}" >&2 - fi - return "${rc}" -} - -go_hostutil_publish_pr() { - ensure_go_run_env - local -a env_args=( - "GOPATH=${GOPATH}" - "GOMODCACHE=${GOMODCACHE}" - "GOCACHE=${GOCACHE}" - ) - - [[ -n "${TERM:-}" ]] && env_args+=("TERM=${TERM}") - [[ -n "${GPG_TTY:-}" ]] && env_args+=("GPG_TTY=${GPG_TTY}") - [[ -n "${GNUPGHOME:-}" ]] && env_args+=("GNUPGHOME=${GNUPGHOME}") - [[ -n "${SSH_AUTH_SOCK:-}" ]] && env_args+=("SSH_AUTH_SOCK=${SSH_AUTH_SOCK}") - [[ -n "${SSH_AGENT_PID:-}" ]] && env_args+=("SSH_AGENT_PID=${SSH_AGENT_PID}") - [[ -n "${SSH_ASKPASS:-}" ]] && env_args+=("SSH_ASKPASS=${SSH_ASKPASS}") - [[ -n "${GIT_ASKPASS:-}" ]] && env_args+=("GIT_ASKPASS=${GIT_ASKPASS}") - [[ -n "${XDG_CONFIG_HOME:-}" ]] && env_args+=("XDG_CONFIG_HOME=${XDG_CONFIG_HOME}") - [[ -n "${XDG_STATE_HOME:-}" ]] && env_args+=("XDG_STATE_HOME=${XDG_STATE_HOME}") - [[ -n "${XDG_CACHE_HOME:-}" ]] && env_args+=("XDG_CACHE_HOME=${XDG_CACHE_HOME}") - [[ -n "${XDG_DATA_HOME:-}" ]] && env_args+=("XDG_DATA_HOME=${XDG_DATA_HOME}") - [[ -n "${XDG_RUNTIME_DIR:-}" ]] && env_args+=("XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}") - [[ -n "${GH_TOKEN:-}" ]] && env_args+=("GH_TOKEN=${GH_TOKEN}") - [[ -n "${GITHUB_TOKEN:-}" ]] && env_args+=("GITHUB_TOKEN=${GITHUB_TOKEN}") - [[ -n "${GH_HOST:-}" ]] && env_args+=("GH_HOST=${GH_HOST}") - [[ -n "${GH_CONFIG_DIR:-}" ]] && env_args+=("GH_CONFIG_DIR=${GH_CONFIG_DIR}") - - run_clean_host_command_in_dir "${ROOT_DIR}" env \ - "${env_args[@]}" \ - "${HOST_GO_BIN}" run ./cmd/workcell-hostutil "$@" -} - -go_colimautil() { - ensure_go_run_env - run_clean_host_command_in_dir "${ROOT_DIR}" env \ - GOPATH="${GOPATH}" \ - GOMODCACHE="${GOMODCACHE}" \ - GOCACHE="${GOCACHE}" \ - "${HOST_GO_BIN}" run ./cmd/workcell-colimautil "$@" -} HOST_GIT_BIN="" HOST_COLIMA_BIN="" From abb0ad02e7738f04dba543f92052adbc7e4c00ba Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 14:52:12 -0400 Subject: [PATCH 2/6] ^d Add docs/launcher-contract.md go-hostutil section (doc only, no code execution impact; markdownlint green; supporting documentation for the D4 go-hostutil extraction) Co-Authored-By: Claude Opus 4.8 --- docs/launcher-contract.md | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/launcher-contract.md b/docs/launcher-contract.md index e37cb264..6037dce7 100644 --- a/docs/launcher-contract.md +++ b/docs/launcher-contract.md @@ -108,3 +108,50 @@ command from a caller-supplied working directory. Called as prints `Missing host working directory: ` to stderr and exits `2`. With no command arguments it is a no-op returning `0`. `HOME` resolution and the pinned `PATH`/locale are identical to `run_clean_host_command()`. + +## Go/Colima host-utility wrappers (`go-hostutil.sh`) + +`scripts/lib/launcher/go-hostutil.sh` invokes the `workcell-hostutil` and +`workcell-colimautil` Go programs on the host via `go run`, always routed +through `run_clean_host_command_in_dir` (from `host-exec.sh`) so the child +executes from `${ROOT_DIR}` under the sanitised `env -i` host environment. Every +helper depends only on `ensure_go_run_env` plus the `GOPATH`/`GOMODCACHE`/ +`GOCACHE` it exports (`scripts/lib/go-run-env.sh`), +`run_clean_host_command_in_dir` (`scripts/lib/launcher/host-exec.sh`), and the +readonly `ROOT_DIR` and `HOST_GO_BIN` globals set in `scripts/workcell` — all +sourced or assigned before the first wrapper call — which makes the module +self-contained. + +### `go_hostutil()` + +Runs `workcell-hostutil` on the host. Calls `ensure_go_run_env`, then executes +`"${HOST_GO_BIN}" run ./cmd/workcell-hostutil "$@"` from `${ROOT_DIR}` via +`run_clean_host_command_in_dir`, forwarding only `GOPATH`/`GOMODCACHE`/`GOCACHE` +into the sanitised environment. All other host context must be passed on argv +because the `env -i` boundary strips inherited environment variables. + +### `run_go_hostutil_preserve_exit()` + +Wraps `go_hostutil()` and recovers the Go child's real exit code. It captures +the child's stderr to a temp file (cleaned up via a `RETURN` trap and +explicitly), and if the stderr ends in an `exit status N` trailer emitted by +`go run`, it substitutes `N` for the generic exit code `1` and strips the +trailer from the forwarded stderr. Non-trailer stderr is re-emitted unchanged +and the resolved exit code is returned. + +### `go_hostutil_publish_pr()` + +Same `go run ./cmd/workcell-hostutil` invocation as `go_hostutil()`, but forwards +an explicit allowlist of terminal, GnuPG, SSH, XDG, and GitHub environment +variables (for example `TERM`, `GPG_TTY`, `GNUPGHOME`, `SSH_AUTH_SOCK`, +`GIT_ASKPASS`, the `XDG_*` dirs, `GH_TOKEN`/`GITHUB_TOKEN`, `GH_HOST`, +`GH_CONFIG_DIR`) — each added only when non-empty — so host-side PR publication +can reach the operator's credentials and interactive signing agents while the +rest of the environment stays sanitised. + +### `go_colimautil()` + +Runs `workcell-colimautil` on the host. Identical structure to `go_hostutil()` +(`ensure_go_run_env`, forwarding `GOPATH`/`GOMODCACHE`/`GOCACHE` from +`${ROOT_DIR}` via `run_clean_host_command_in_dir`), targeting +`./cmd/workcell-colimautil` instead. From 918eddbe895e67af4c58cef198cf637d51b3a0e2 Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 14:52:12 -0400 Subject: [PATCH 3/6] ^b Track go-hostutil.sh in the control-plane manifest generator (P2) (the extracted scripts/lib/launcher/go-hostutil.sh is now sourced launcher code, but GenerateControlPlaneManifest in internal/metadatautil/core.go only listed the older host artifacts, so the new module was outside the signed/provenance-verified manifest and go-hostutil changes wouldn't be caught; add it to hostArtifacts and regenerate control-plane-manifest.json) (manifest verify + go test green; supporting the D4 go-hostutil extraction - closes the provenance gap the move opened) Co-Authored-By: Claude Opus 4.8 --- internal/metadatautil/core.go | 1 + runtime/container/control-plane-manifest.json | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/metadatautil/core.go b/internal/metadatautil/core.go index 9f8b62ee..4a1c6d65 100644 --- a/internal/metadatautil/core.go +++ b/internal/metadatautil/core.go @@ -469,6 +469,7 @@ func GenerateControlPlaneManifest(rootDir, outputPath string) error { "scripts/lib/extract_direct_mounts", "scripts/lib/launcher/host-detect.sh", "scripts/lib/launcher/host-exec.sh", + "scripts/lib/launcher/go-hostutil.sh", "scripts/lib/render_injection_bundle", "scripts/lib/sessionctl-shim.sh", "scripts/lib/shellproto.sh", diff --git a/runtime/container/control-plane-manifest.json b/runtime/container/control-plane-manifest.json index 83cccd83..ddd194c7 100644 --- a/runtime/container/control-plane-manifest.json +++ b/runtime/container/control-plane-manifest.json @@ -2,7 +2,7 @@ "host_artifacts": [ { "repo_path": "scripts/workcell", - "sha256": "8cad3d7c2f99071a346a478a45915c6a3ff2997c94133c6c8384e3893b79eec3" + "sha256": "fbfe69fb7901b5afcff7dc304024c4bee50d4be81186494baca4fa56bd4621fe" }, { "repo_path": "scripts/check-publish-commit-signatures.sh", @@ -20,6 +20,10 @@ "repo_path": "scripts/lib/launcher/host-exec.sh", "sha256": "475bfcf95c6e3d20631ba2aacd43261fa116323191b1898a47afbf35c96fbd50" }, + { + "repo_path": "scripts/lib/launcher/go-hostutil.sh", + "sha256": "d640acf4833ea78378db353fad430c574f500bbbc5bc9cbfde07e96b842b8020" + }, { "repo_path": "scripts/lib/render_injection_bundle", "sha256": "f1057951d6f65e12d82a7bc2b5002afde5664670d8f4ed7d9bc289bbec5f5901" From eaa477be0d6e4d3c32e31e4f0900d35c40c170d6 Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 14:59:54 -0400 Subject: [PATCH 4/6] ^r Resolve HOST_GO_BIN inside go-hostutil.sh instead of suppressing SC2034 (the extracted module is HOST_GO_BIN's sole consumer, so resolving it in scripts/workcell left the var used only across a source=/dev/null boundary that ShellCheck can't see, requiring a SC2034 disable; move the resolve_fixed_host_tool assignment into go-hostutil.sh - resolve_fixed_host_tool is provided by the host-exec.sh module sourced immediately before - so set and use co-locate and the suppression is removed; regenerated the manifest for the module change) (shellcheck -x clean with no disable, behavior-preserving - same resolution, same abort-on-missing-go; supporting the go-hostutil extraction, prefers refactor over alert suppression per repo policy) --- runtime/container/control-plane-manifest.json | 4 ++-- scripts/lib/launcher/go-hostutil.sh | 13 +++++++++---- scripts/workcell | 4 ---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/runtime/container/control-plane-manifest.json b/runtime/container/control-plane-manifest.json index ddd194c7..85427ceb 100644 --- a/runtime/container/control-plane-manifest.json +++ b/runtime/container/control-plane-manifest.json @@ -2,7 +2,7 @@ "host_artifacts": [ { "repo_path": "scripts/workcell", - "sha256": "fbfe69fb7901b5afcff7dc304024c4bee50d4be81186494baca4fa56bd4621fe" + "sha256": "6e5f82003d66f31a48fb6e2295cb15cec2bc391a3d58fdf21093c89cb4547c08" }, { "repo_path": "scripts/check-publish-commit-signatures.sh", @@ -22,7 +22,7 @@ }, { "repo_path": "scripts/lib/launcher/go-hostutil.sh", - "sha256": "d640acf4833ea78378db353fad430c574f500bbbc5bc9cbfde07e96b842b8020" + "sha256": "775d3dd6e5d946229d6eb38ccfcc36f6a89711662c4fcd2df9d097a8741e0116" }, { "repo_path": "scripts/lib/render_injection_bundle", diff --git a/scripts/lib/launcher/go-hostutil.sh b/scripts/lib/launcher/go-hostutil.sh index 96965f62..60081930 100755 --- a/scripts/lib/launcher/go-hostutil.sh +++ b/scripts/lib/launcher/go-hostutil.sh @@ -13,16 +13,21 @@ # scripts/lib/launcher/host-exec.sh. They depend only on # ensure_go_run_env plus the GOPATH/GOMODCACHE/GOCACHE it exports # (scripts/lib/go-run-env.sh), run_clean_host_command_in_dir -# (scripts/lib/launcher/host-exec.sh), and the readonly ROOT_DIR and -# HOST_GO_BIN globals set in scripts/workcell — all sourced or assigned -# before the first wrapper call — so they are a self-contained, -# behaviour-preserving unit. run_go_hostutil_preserve_exit additionally +# (scripts/lib/launcher/host-exec.sh), and the readonly ROOT_DIR global +# set in scripts/workcell — all sourced or assigned before the first +# wrapper call — so they are a self-contained, behaviour-preserving unit. +# HOST_GO_BIN is resolved by this module (below) since it is the sole +# consumer; resolve_fixed_host_tool comes from the host-exec.sh module +# sourced immediately before this one. +# run_go_hostutil_preserve_exit additionally # recovers the Go child's real exit code from its `exit status N` stderr # trailer. go_hostutil_publish_pr forwards an explicit allowlist of # terminal/GnuPG/SSH/XDG/GitHub environment variables (read at call time) # so host-side PR publication can reach the operator's credentials. See # docs/launcher-contract.md for the module contract. +HOST_GO_BIN="$(resolve_fixed_host_tool go /opt/homebrew/bin/go /usr/local/go/bin/go /usr/local/bin/go /usr/bin/go)" + go_hostutil() { ensure_go_run_env run_clean_host_command_in_dir "${ROOT_DIR}" env \ diff --git a/scripts/workcell b/scripts/workcell index 622cdb21..615c9f17 100755 --- a/scripts/workcell +++ b/scripts/workcell @@ -70,10 +70,6 @@ source "${ROOT_DIR}/scripts/lib/launcher/host-detect.sh" source "${ROOT_DIR}/scripts/lib/launcher/host-exec.sh" # shellcheck source=/dev/null source "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" -# HOST_GO_BIN is consumed by the go-hostutil.sh wrappers sourced above; the -# source=/dev/null directive hides that cross-file use from ShellCheck. -# shellcheck disable=SC2034 -HOST_GO_BIN="$(resolve_fixed_host_tool go /opt/homebrew/bin/go /usr/local/go/bin/go /usr/local/bin/go /usr/bin/go)" REAL_HOME="$(resolve_workcell_real_home)" case "$(uname -s 2>/dev/null || true)" in Darwin) From 8d9c244e106202a4d0762cb449166a4dfd29955d Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 15:12:20 -0400 Subject: [PATCH 5/6] ^b Fix go-hostutil ordering invariant and repoint repo validators to the module (P1 from Codex on #396) (moving go_hostutil/run_go_hostutil_preserve_exit out of scripts/workcell violated the security ordering invariant that REAL_HOME and WORKCELL_GO_CACHE_ROOT must be established before go_hostutil is defined, and broke three repo-owned validators that pinned those functions to scripts/workcell; behavior-preserving - only the source-line position and the validators' target paths change) Ordering invariant: the go_hostutil/go_colimautil wrappers are defined when scripts/workcell sources scripts/lib/launcher/go-hostutil.sh, so that source line now runs AFTER REAL_HOME="$(resolve_workcell_real_home)" and the WORKCELL_GO_CACHE_ROOT export block (moved down from just after host-exec.sh to immediately after the esac). host-exec.sh and go-run-env.sh stay sourced before, so HOST_GO_BIN resolution in the module still works; nothing between the old and new source position uses go_hostutil or HOST_GO_BIN. Validators retargeted to follow the moved code: - internal/testkit/security_boundary_test.go: the ordering assertion now targets the go-hostutil.sh source-line index (must come after REAL_HOME and WORKCELL_GO_CACHE_ROOT), preserving the existing presence+order checks, and additionally asserts go_hostutil() is defined in the module file. - scripts/verify-invariants.sh: extract_top_level_bash_function for run_go_hostutil_preserve_exit and the go_hostutil-body rg checks now read scripts/lib/launcher/go-hostutil.sh instead of scripts/workcell. - runtime/container/control-plane-manifest.json regenerated for the workcell source-line move. Co-Authored-By: Claude Opus 4.8 --- internal/testkit/security_boundary_test.go | 29 ++++++++++++++----- runtime/container/control-plane-manifest.json | 2 +- scripts/verify-invariants.sh | 14 ++++----- scripts/workcell | 8 +++-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/internal/testkit/security_boundary_test.go b/internal/testkit/security_boundary_test.go index aaede920..f290999d 100644 --- a/internal/testkit/security_boundary_test.go +++ b/internal/testkit/security_boundary_test.go @@ -185,15 +185,30 @@ func TestWorkcellBootstrapResolvesRealHomeBeforeGoHostutil(t *testing.T) { t.Fatalf("%s exports WORKCELL_GO_CACHE_ROOT before REAL_HOME is resolved", scriptPath) } - goHostutilIndex := strings.Index(script, `go_hostutil() {`) - if goHostutilIndex == -1 { - t.Fatalf("%s must define go_hostutil", scriptPath) + // go_hostutil now lives in scripts/lib/launcher/go-hostutil.sh, so it + // becomes defined at the point scripts/workcell sources that module. The + // ordering invariant therefore targets the source line: the module must be + // sourced only after REAL_HOME is resolved and WORKCELL_GO_CACHE_ROOT is + // exported, so the go_hostutil/go_colimautil wrappers can never run with an + // unsanitised host home or Go cache root. + goHostutilSourceIndex := strings.Index(script, `source "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh"`) + if goHostutilSourceIndex == -1 { + t.Fatalf("%s must source scripts/lib/launcher/go-hostutil.sh", scriptPath) } - if realHomeIndex > goHostutilIndex { - t.Fatalf("%s resolves REAL_HOME after go_hostutil is defined; want bootstrap home resolved first", scriptPath) + if realHomeIndex > goHostutilSourceIndex { + t.Fatalf("%s resolves REAL_HOME after go-hostutil.sh is sourced; want bootstrap home resolved first", scriptPath) } - if cacheRootIndex > goHostutilIndex { - t.Fatalf("%s exports WORKCELL_GO_CACHE_ROOT after go_hostutil is defined; want cache root fixed first", scriptPath) + if cacheRootIndex > goHostutilSourceIndex { + t.Fatalf("%s exports WORKCELL_GO_CACHE_ROOT after go-hostutil.sh is sourced; want cache root fixed first", scriptPath) + } + + modulePath := filepath.Join(repoRoot(t), "scripts", "lib", "launcher", "go-hostutil.sh") + moduleContent, err := os.ReadFile(modulePath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(moduleContent), `go_hostutil() {`) { + t.Fatalf("%s must define go_hostutil", modulePath) } } diff --git a/runtime/container/control-plane-manifest.json b/runtime/container/control-plane-manifest.json index 85427ceb..e0f21b75 100644 --- a/runtime/container/control-plane-manifest.json +++ b/runtime/container/control-plane-manifest.json @@ -2,7 +2,7 @@ "host_artifacts": [ { "repo_path": "scripts/workcell", - "sha256": "6e5f82003d66f31a48fb6e2295cb15cec2bc391a3d58fdf21093c89cb4547c08" + "sha256": "6e9c2fa71b8b1c5f971a710c9e4e33769bcf2210a61ab2c8f1420caebeedf002" }, { "repo_path": "scripts/check-publish-commit-signatures.sh", diff --git a/scripts/verify-invariants.sh b/scripts/verify-invariants.sh index 3963dcaf..01b0a114 100755 --- a/scripts/verify-invariants.sh +++ b/scripts/verify-invariants.sh @@ -2766,7 +2766,7 @@ WORKCELL_COLIMA_TIMEOUT_HARNESS="${BARRIER_VERIFY_ROOT}/workcell-colima-timeout- printf '\n' extract_top_level_bash_function "${ROOT_DIR}/scripts/workcell" terminate_process_tree_by_pid printf '\n' - extract_top_level_bash_function "${ROOT_DIR}/scripts/workcell" run_go_hostutil_preserve_exit + extract_top_level_bash_function "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" run_go_hostutil_preserve_exit printf '\n' extract_top_level_bash_function "${ROOT_DIR}/scripts/workcell" run_host_colima_with_timeout printf '\n' @@ -2843,12 +2843,12 @@ WORKCELL_RUNTIME_BUILD_RETRY_HARNESS="${BARRIER_VERIFY_ROOT}/workcell-runtime-bu } >"${WORKCELL_RUNTIME_BUILD_RETRY_HARNESS}" bash "${WORKCELL_RUNTIME_BUILD_RETRY_HARNESS}" -if ! rg -q 'run_clean_host_command_in_dir "\$\{ROOT_DIR\}" env' "${ROOT_DIR}/scripts/workcell" || - ! rg -q 'GOPATH="\$\{GOPATH\}"' "${ROOT_DIR}/scripts/workcell" || - ! rg -q 'GOMODCACHE="\$\{GOMODCACHE\}"' "${ROOT_DIR}/scripts/workcell" || - ! rg -q 'GOCACHE="\$\{GOCACHE\}"' "${ROOT_DIR}/scripts/workcell" || - ! rg -q '"\$\{HOST_GO_BIN\}" run ./cmd/workcell-hostutil "\$@"' "${ROOT_DIR}/scripts/workcell"; then - echo "Expected scripts/workcell to invoke the bootstrap Go helper from the repo root under a scrubbed environment with explicit Go caches" >&2 +if ! rg -q 'run_clean_host_command_in_dir "\$\{ROOT_DIR\}" env' "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" || + ! rg -q 'GOPATH="\$\{GOPATH\}"' "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" || + ! rg -q 'GOMODCACHE="\$\{GOMODCACHE\}"' "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" || + ! rg -q 'GOCACHE="\$\{GOCACHE\}"' "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" || + ! rg -q '"\$\{HOST_GO_BIN\}" run ./cmd/workcell-hostutil "\$@"' "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh"; then + echo "Expected scripts/lib/launcher/go-hostutil.sh to invoke the bootstrap Go helper from the repo root under a scrubbed environment with explicit Go caches" >&2 exit 1 fi diff --git a/scripts/workcell b/scripts/workcell index 615c9f17..b867ab6a 100755 --- a/scripts/workcell +++ b/scripts/workcell @@ -68,8 +68,6 @@ source "${ROOT_DIR}/scripts/lib/sessionctl-shim.sh" source "${ROOT_DIR}/scripts/lib/launcher/host-detect.sh" # shellcheck source=/dev/null source "${ROOT_DIR}/scripts/lib/launcher/host-exec.sh" -# shellcheck source=/dev/null -source "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" REAL_HOME="$(resolve_workcell_real_home)" case "$(uname -s 2>/dev/null || true)" in Darwin) @@ -79,6 +77,12 @@ case "$(uname -s 2>/dev/null || true)" in export WORKCELL_GO_CACHE_ROOT="${XDG_CACHE_HOME:-${REAL_HOME}/.cache}/workcell/go" ;; esac +# Sourced after REAL_HOME and WORKCELL_GO_CACHE_ROOT are established so the +# go_hostutil/go_colimautil wrappers this module defines can never be invoked +# with an unsanitised host home or Go cache root (security ordering invariant; +# see internal/testkit/security_boundary_test.go). +# shellcheck source=/dev/null +source "${ROOT_DIR}/scripts/lib/launcher/go-hostutil.sh" HOST_GIT_BIN="" HOST_COLIMA_BIN="" From 396dfb8cb39c18505c5b918126cbb1597753f66f Mon Sep 17 00:00:00 2001 From: Omkhar Arasaratnam Date: Sun, 5 Jul 2026 15:23:11 -0400 Subject: [PATCH 6/6] ^d Correct HOST_GO_BIN ownership in launcher-contract.md (P3) (the go-hostutil extraction moved HOST_GO_BIN's resolution into scripts/lib/launcher/go-hostutil.sh, but the contract doc still described it as a global set in scripts/workcell; update the module-contract paragraph to state ROOT_DIR is set in scripts/workcell while HOST_GO_BIN is resolved by the module itself via resolve_fixed_host_tool from host-exec.sh, so the doc matches the implementation) (markdownlint green, doc-only; doc/code lockstep) --- docs/launcher-contract.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/launcher-contract.md b/docs/launcher-contract.md index 6037dce7..a0d4208f 100644 --- a/docs/launcher-contract.md +++ b/docs/launcher-contract.md @@ -118,9 +118,11 @@ executes from `${ROOT_DIR}` under the sanitised `env -i` host environment. Every helper depends only on `ensure_go_run_env` plus the `GOPATH`/`GOMODCACHE`/ `GOCACHE` it exports (`scripts/lib/go-run-env.sh`), `run_clean_host_command_in_dir` (`scripts/lib/launcher/host-exec.sh`), and the -readonly `ROOT_DIR` and `HOST_GO_BIN` globals set in `scripts/workcell` — all -sourced or assigned before the first wrapper call — which makes the module -self-contained. +`ROOT_DIR` global set in `scripts/workcell`. `HOST_GO_BIN` is resolved by this +module itself (via `resolve_fixed_host_tool` from `host-exec.sh`, sourced +immediately before), since these wrappers are its sole consumer — so every +dependency is sourced or assigned before the first wrapper call, which makes the +module self-contained. ### `go_hostutil()`