Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/launcher-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,52 @@ command from a caller-supplied working directory. Called as
prints `Missing host working directory: <dir>` 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
`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()`

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.
1 change: 1 addition & 0 deletions internal/metadatautil/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 22 additions & 7 deletions internal/testkit/security_boundary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
6 changes: 5 additions & 1 deletion runtime/container/control-plane-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"host_artifacts": [
{
"repo_path": "scripts/workcell",
"sha256": "8cad3d7c2f99071a346a478a45915c6a3ff2997c94133c6c8384e3893b79eec3"
"sha256": "6e9c2fa71b8b1c5f971a710c9e4e33769bcf2210a61ab2c8f1420caebeedf002"
},
{
"repo_path": "scripts/check-publish-commit-signatures.sh",
Expand All @@ -20,6 +20,10 @@
"repo_path": "scripts/lib/launcher/host-exec.sh",
"sha256": "475bfcf95c6e3d20631ba2aacd43261fa116323191b1898a47afbf35c96fbd50"
},
{
"repo_path": "scripts/lib/launcher/go-hostutil.sh",
"sha256": "775d3dd6e5d946229d6eb38ccfcc36f6a89711662c4fcd2df9d097a8741e0116"
},
{
"repo_path": "scripts/lib/render_injection_bundle",
"sha256": "f1057951d6f65e12d82a7bc2b5002afde5664670d8f4ed7d9bc289bbec5f5901"
Expand Down
1 change: 1 addition & 0 deletions scripts/dev-quick-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 105 additions & 0 deletions scripts/lib/launcher/go-hostutil.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/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 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 \
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 "$@"
}
1 change: 1 addition & 0 deletions scripts/validate-repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 7 additions & 7 deletions scripts/verify-invariants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down
82 changes: 6 additions & 76 deletions scripts/workcell
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +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"
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)
Expand All @@ -78,81 +77,12 @@ 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 "$@"
}
# 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=""
Expand Down
Loading