From a00aa6628b329a726754a67eca6d1dc7fd168543 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:59:31 +0000 Subject: [PATCH 1/3] Initial plan From 5977eaa28d3a9876f886a8f56413ee3497b78a2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:18:04 +0000 Subject: [PATCH 2/3] Stage behavior-defined engine CLIs into docker-sbx visible path Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...fix-behavior-engine-docker-sbx-cli-path.md | 7 ++ .../workflows/daily-arxiv-researcher.lock.yml | 79 +++++++++++++++++-- .../workflows/daily-code-debt-aider.lock.yml | 2 +- .github/workflows/daily-code-metrics.lock.yml | 79 +++++++++++++++++-- .github/workflows/shared/crush.md | 34 +++++++- .github/workflows/smoke-crush.lock.yml | 70 ++++++++++++++-- pkg/workflow/behavior_defined_engine.go | 19 +++++ pkg/workflow/docker_sbx_test.go | 67 ++++++++++++++++ 8 files changed, 330 insertions(+), 27 deletions(-) create mode 100644 .changeset/fix-behavior-engine-docker-sbx-cli-path.md diff --git a/.changeset/fix-behavior-engine-docker-sbx-cli-path.md b/.changeset/fix-behavior-engine-docker-sbx-cli-path.md new file mode 100644 index 00000000000..bec15d318c2 --- /dev/null +++ b/.changeset/fix-behavior-engine-docker-sbx-cli-path.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Fix `ENOENT` failures for behavior-defined engines (such as Crush) running on the `docker-sbx` and `cloud-hypervisor` sandbox runtimes. These runtimes do not mount the runner tool cache into the sandbox, so an engine CLI installed with `npm install -g` was invisible to the agent process even though the host-side install and verify steps succeeded. The engine CLI is now also staged into `${RUNNER_TEMP}/gh-aw/engine-cli` and that `bin` directory is prepended to the sandbox `PATH`, matching the Claude and Codex engines. + +The Crush harness now resolves its CLI on `PATH` before spawning it and fails with `crush not found in sandbox PATH: ` instead of a bare `spawnSync crush ENOENT`. diff --git a/.github/workflows/daily-arxiv-researcher.lock.yml b/.github/workflows/daily-arxiv-researcher.lock.yml index 573f2b07d29..a37992b5822 100644 --- a/.github/workflows/daily-arxiv-researcher.lock.yml +++ b/.github/workflows/daily-arxiv-researcher.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"41adfdf8a217bbd72ffa7d2d3fac1ce0e3e317a39a43851fce4be52159ff1bb4","body_hash":"8a0ae0a7a36920221c9cfc8d390ce6ca8ee22847f2bafa90bd7d62995bfb0e9d","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1c722f9789b54d464ae08c0534521df56dc25139772b30e1e1ba0d2c8726a725","body_hash":"8a0ae0a7a36920221c9cfc8d390ce6ca8ee22847f2bafa90bd7d62995bfb0e9d","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["DOCKER_PAT","DOCKER_USERNAME","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -480,6 +480,13 @@ jobs: run: npm install -g @charmland/crush@0.88.0 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' + - name: Install Crush in docker-sbx path + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/engine-cli/bin" + npm install --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @charmland/crush@0.88.0 + ln -sf "../node_modules/.bin/crush" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/crush" + env: + NPM_CONFIG_MIN_RELEASE_AGE: '3' - name: Verify Crush CLI installation run: crush --version - name: Determine automatic lockdown mode for GitHub MCP Server @@ -851,8 +858,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -862,6 +869,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -937,8 +970,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", @@ -1070,7 +1105,7 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --container-runtime sbx --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs crush run --quiet' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs crush run --quiet' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ github.token }} @@ -1649,8 +1684,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -1660,6 +1695,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -1735,8 +1796,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", diff --git a/.github/workflows/daily-code-debt-aider.lock.yml b/.github/workflows/daily-code-debt-aider.lock.yml index 9288faddeb9..06f09d1bed2 100644 --- a/.github/workflows/daily-code-debt-aider.lock.yml +++ b/.github/workflows/daily-code-debt-aider.lock.yml @@ -879,7 +879,7 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --container-runtime sbx --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull --difc-proxy-host awmg-cli-proxy:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/aider_harness.cjs aider --yes-always --no-auto-commits --no-check-update --no-show-release-notes --no-detect-urls --no-pretty --no-stream --no-fancy-input --analytics-disable --openai-api-base http://172.30.0.30:10002 --set-env OPENAI_BASE_URL=http://172.30.0.30:10002 --openai-api-key awf-copilot-proxy' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/aider_harness.cjs aider --yes-always --no-auto-commits --no-check-update --no-show-release-notes --no-detect-urls --no-pretty --no-stream --no-fancy-input --analytics-disable --openai-api-base http://172.30.0.30:10002 --set-env OPENAI_BASE_URL=http://172.30.0.30:10002 --openai-api-key awf-copilot-proxy' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AIDER_ANALYTICS_DISABLE: true AIDER_CHECK_UPDATE: false diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index 525c4f33073..4f72b0e626e 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"19662488c7f5e940e136303f1357de7854197874e33a3a3174e9c4efac6df916","body_hash":"999291c33885ecbdbc7a407fb6f399b412507fb4ab69814ac9586f39db4fb4ca","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b350935d6806a46a1adcd80a8ae5e4b8930e4732183e53c4d3090f428776ea92","body_hash":"999291c33885ecbdbc7a407fb6f399b412507fb4ab69814ac9586f39db4fb4ca","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["DOCKER_PAT","DOCKER_USERNAME","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -564,6 +564,13 @@ jobs: run: npm install -g @charmland/crush@0.88.0 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' + - name: Install Crush in docker-sbx path + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/engine-cli/bin" + npm install --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @charmland/crush@0.88.0 + ln -sf "../node_modules/.bin/crush" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/crush" + env: + NPM_CONFIG_MIN_RELEASE_AGE: '3' - name: Verify Crush CLI installation run: crush --version - name: Determine automatic lockdown mode for GitHub MCP Server @@ -961,8 +968,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -972,6 +979,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -1047,8 +1080,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", @@ -1180,7 +1215,7 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --container-runtime sbx --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs crush run --quiet' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs crush run --quiet' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ github.token }} @@ -1987,8 +2022,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -1998,6 +2033,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -2073,8 +2134,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", diff --git a/.github/workflows/shared/crush.md b/.github/workflows/shared/crush.md index 59820b46fba..4c1f1b7c14f 100644 --- a/.github/workflows/shared/crush.md +++ b/.github/workflows/shared/crush.md @@ -132,8 +132,8 @@ engine: fs.chmodSync(configPath, 0o600); harness-script: | const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -143,6 +143,32 @@ engine: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -218,8 +244,10 @@ engine: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", diff --git a/.github/workflows/smoke-crush.lock.yml b/.github/workflows/smoke-crush.lock.yml index 81348756846..943cf9a4ee7 100644 --- a/.github/workflows/smoke-crush.lock.yml +++ b/.github/workflows/smoke-crush.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"038fdcb95b5efbf6a2ef3f95ebbad93e253d2518b3deeb569031100acd22fc2e","body_hash":"e5ee7908c0ded99864054d63fdfda9d352a7f6bbd6ae09ed51ee0f31773ecbf1","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a633096cdec4da70fed4380503e31ced967f203e7820ad4fdf78b4e617c356f5","body_hash":"e5ee7908c0ded99864054d63fdfda9d352a7f6bbd6ae09ed51ee0f31773ecbf1","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -982,8 +982,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -993,6 +993,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -1068,8 +1094,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", @@ -1876,8 +1904,8 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/actions" cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" const { spawnSync } = require("child_process"); - const { readFileSync, writeFileSync } = require("fs"); - const { join } = require("path"); + const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); + const { isAbsolute, join } = require("path"); const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); @@ -1887,6 +1915,32 @@ jobs: if (result.status !== 0) throw new Error(`${action} failed with exit code ${result.status ?? "unknown"}`); }; + // Preflight the CLI lookup inside the sandbox: the binary is installed on the runner + // host, so a missing sandbox mount surfaces as a bare ENOENT from spawnSync. + const resolveCommand = name => { + const searchPath = process.env.PATH || ""; + if (name.includes("/")) { + const candidate = isAbsolute(name) ? name : join(process.cwd(), name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + throw new Error(`${name} not executable in sandbox: ${candidate}`); + } + } + for (const dir of searchPath.split(":")) { + if (!dir) continue; + const candidate = join(dir, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + continue; + } + } + throw new Error(`${name} not found in sandbox PATH: ${searchPath}`); + }; + const main = async () => { const selectedModel = process.env.CRUSH_MODEL; if (!selectedModel || !selectedModel.includes("/")) { @@ -1962,8 +2016,10 @@ jobs: const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) throw new Error("GH_AW_PROMPT is required"); const prompt = readFileSync(promptPath, "utf8"); + const executable = resolveCommand(command); + log(`resolved ${command} to ${executable}`); fail( - spawnSync(command, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { + spawnSync(executable, [...commandArgs, "--model", `awf-proxy/${model}`, prompt], { cwd: workspace, env: process.env, stdio: "inherit", diff --git a/pkg/workflow/behavior_defined_engine.go b/pkg/workflow/behavior_defined_engine.go index 87a5f672e7f..fd4d78a5081 100644 --- a/pkg/workflow/behavior_defined_engine.go +++ b/pkg/workflow/behavior_defined_engine.go @@ -209,6 +209,20 @@ func (e *BehaviorDefinedEngine) GetInstallationSteps(workflowData *WorkflowData) CooldownEnabled: install.Cooldown, }, ) + // microVM runtimes (docker-sbx/cloud-hypervisor) do not mount the runner tool cache, + // so a global npm install is invisible inside the sandbox. Stage a second copy of the + // CLI under ${RUNNER_TEMP}/gh-aw/engine-cli, which is mounted into the sandbox, exactly + // as the Claude and Codex engines do. + if install.BinaryName != "" && (isDockerSbxRuntime(workflowData) || isCloudHypervisorRuntime(workflowData)) { + npmSteps = append(npmSteps, GenerateDockerSbxNpmCLIInstallStep( + install.PackageName, + version, + install.StepName+" in docker-sbx path", + install.BinaryName, + install.PostInstallScripts, + install.Cooldown, + )) + } if install.VerifyCommand != "" { npmSteps = append(npmSteps, GitHubActionStep{ " - name: " + install.VerifyStepName, @@ -626,6 +640,11 @@ func (e *BehaviorDefinedEngine) buildFirewallCommand(exec *EngineExecutionDefini // from the YAML env block, but Bun (and other runtimes) also check the // lowercase variant, so we export it explicitly from the uppercase value. engineCommandWithPath := fmt.Sprintf("export no_proxy=\"${NO_PROXY:-}\" && %s && %s", GetNpmBinPathSetup(), engineCommand) + // microVM runtimes stage the engine CLI under ${RUNNER_TEMP}/gh-aw/engine-cli/bin + // because the runner tool cache is not mounted inside the sandbox. + if dockerSbxCLIPath := GetDockerSbxNpmCLIPathSetup(workflowData); dockerSbxCLIPath != "" { + engineCommandWithPath = fmt.Sprintf("%s && %s", dockerSbxCLIPath, engineCommandWithPath) + } if mcpCLIPath := GetMCPCLIPathSetup(workflowData); mcpCLIPath != "" { engineCommandWithPath = fmt.Sprintf("%s && %s", mcpCLIPath, engineCommandWithPath) } diff --git a/pkg/workflow/docker_sbx_test.go b/pkg/workflow/docker_sbx_test.go index 8eb0dc61671..5cf924d844d 100644 --- a/pkg/workflow/docker_sbx_test.go +++ b/pkg/workflow/docker_sbx_test.go @@ -880,3 +880,70 @@ func TestDockerSbxRuntimeInstallFalseOmitsInstallSteps(t *testing.T) { assert.NotContains(t, content, "docker_sbx_daemon.sh", "daemon step must be omitted when runtime-install: false") assert.NotContains(t, content, "docker-sbx pre-flight smoke test", "pre-flight must be omitted when runtime-install: false") } + +// TestDockerSbxBehaviorDefinedEngineCLIWiring verifies that behavior-defined engines +// installed through npm (e.g. Crush) also stage their CLI into the sbx-visible path and +// prepend it to the sandbox PATH. Without this the CLI is only present in the runner tool +// cache, which is not mounted into the microVM, and the harness fails with ENOENT. +func TestDockerSbxBehaviorDefinedEngineCLIWiring(t *testing.T) { + def := &EngineDefinition{ + ID: "sbxcrush", + DisplayName: "SbxCrush", + Behaviors: &EngineBehaviorDefinition{ + Installation: &EngineInstallationDefinition{ + PackageManager: "npm", + PackageName: "@charmland/crush", + Version: "0.88.0", + StepName: "Install SbxCrush", + BinaryName: "sbxcrush", + IncludeNodeSetup: true, + PostInstallScripts: true, + }, + Execution: &EngineExecutionDefinition{ + CommandName: "sbxcrush", + Args: []string{"run"}, + StepName: "Execute SbxCrush CLI", + }, + HarnessScript: "// harness\n", + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + sbxWorkflow := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ID: "sbxcrush"}, + SandboxConfig: &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: AgentRuntimeDockerSbx, SudoExplicitlyEnabled: true}}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + + t.Run("install and execution use sbx-visible CLI path", func(t *testing.T) { + installContent := strings.Join(flattenSteps(engine.GetInstallationSteps(sbxWorkflow)), "\n") + assert.Contains(t, installContent, `npm install --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @charmland/crush@0.88.0`) + assert.Contains(t, installContent, `ln -sf "../node_modules/.bin/sbxcrush" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/sbxcrush"`) + + execSteps := engine.GetExecutionSteps(sbxWorkflow, "/tmp/gh-aw/test.log") + require.NotEmpty(t, execSteps) + execContent := strings.Join(execSteps[len(execSteps)-1], "\n") + assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) + }) + + t.Run("non-microVM runtimes keep the host install only", func(t *testing.T) { + defaultWorkflow := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ID: "sbxcrush"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + installContent := strings.Join(flattenSteps(engine.GetInstallationSteps(defaultWorkflow)), "\n") + assert.NotContains(t, installContent, `${RUNNER_TEMP}/gh-aw/engine-cli`) + + execSteps := engine.GetExecutionSteps(defaultWorkflow, "/tmp/gh-aw/test.log") + require.NotEmpty(t, execSteps) + execContent := strings.Join(execSteps[len(execSteps)-1], "\n") + assert.NotContains(t, execContent, `${RUNNER_TEMP}/gh-aw/engine-cli/bin`) + }) +} From 7f56f02f48260aa37114756f67e43155ec32382d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:55:27 +0000 Subject: [PATCH 3/3] Address behavior engine staging review Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/behavior_defined_engine.go | 8 ++++-- pkg/workflow/docker_sbx_test.go | 37 +++++++++++++------------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pkg/workflow/behavior_defined_engine.go b/pkg/workflow/behavior_defined_engine.go index fd4d78a5081..55cb9a9e956 100644 --- a/pkg/workflow/behavior_defined_engine.go +++ b/pkg/workflow/behavior_defined_engine.go @@ -213,12 +213,16 @@ func (e *BehaviorDefinedEngine) GetInstallationSteps(workflowData *WorkflowData) // so a global npm install is invisible inside the sandbox. Stage a second copy of the // CLI under ${RUNNER_TEMP}/gh-aw/engine-cli, which is mounted into the sandbox, exactly // as the Claude and Codex engines do. - if install.BinaryName != "" && (isDockerSbxRuntime(workflowData) || isCloudHypervisorRuntime(workflowData)) { + binaryName := install.BinaryName + if binaryName == "" && behavior.Execution != nil { + binaryName = behavior.Execution.CommandName + } + if binaryName != "" && (isDockerSbxRuntime(workflowData) || isCloudHypervisorRuntime(workflowData)) { npmSteps = append(npmSteps, GenerateDockerSbxNpmCLIInstallStep( install.PackageName, version, install.StepName+" in docker-sbx path", - install.BinaryName, + binaryName, install.PostInstallScripts, install.Cooldown, )) diff --git a/pkg/workflow/docker_sbx_test.go b/pkg/workflow/docker_sbx_test.go index 5cf924d844d..e7f74818269 100644 --- a/pkg/workflow/docker_sbx_test.go +++ b/pkg/workflow/docker_sbx_test.go @@ -895,7 +895,6 @@ func TestDockerSbxBehaviorDefinedEngineCLIWiring(t *testing.T) { PackageName: "@charmland/crush", Version: "0.88.0", StepName: "Install SbxCrush", - BinaryName: "sbxcrush", IncludeNodeSetup: true, PostInstallScripts: true, }, @@ -910,25 +909,27 @@ func TestDockerSbxBehaviorDefinedEngineCLIWiring(t *testing.T) { engine, err := NewBehaviorDefinedEngine(def) require.NoError(t, err) - sbxWorkflow := &WorkflowData{ - Name: "test-workflow", - EngineConfig: &EngineConfig{ID: "sbxcrush"}, - SandboxConfig: &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: AgentRuntimeDockerSbx, SudoExplicitlyEnabled: true}}, - NetworkPermissions: &NetworkPermissions{ - Firewall: &FirewallConfig{Enabled: true}, - }, - } + for _, runtime := range []AgentRuntime{AgentRuntimeDockerSbx, AgentRuntimeCloudHypervisor} { + t.Run(string(runtime)+" install and execution use sbx-visible CLI path", func(t *testing.T) { + sbxWorkflow := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ID: "sbxcrush"}, + SandboxConfig: &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: runtime, SudoExplicitlyEnabled: true}}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } - t.Run("install and execution use sbx-visible CLI path", func(t *testing.T) { - installContent := strings.Join(flattenSteps(engine.GetInstallationSteps(sbxWorkflow)), "\n") - assert.Contains(t, installContent, `npm install --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @charmland/crush@0.88.0`) - assert.Contains(t, installContent, `ln -sf "../node_modules/.bin/sbxcrush" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/sbxcrush"`) + installContent := strings.Join(flattenSteps(engine.GetInstallationSteps(sbxWorkflow)), "\n") + assert.Contains(t, installContent, `npm install --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @charmland/crush@0.88.0`) + assert.Contains(t, installContent, `ln -sf "../node_modules/.bin/sbxcrush" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/sbxcrush"`) - execSteps := engine.GetExecutionSteps(sbxWorkflow, "/tmp/gh-aw/test.log") - require.NotEmpty(t, execSteps) - execContent := strings.Join(execSteps[len(execSteps)-1], "\n") - assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) - }) + execSteps := engine.GetExecutionSteps(sbxWorkflow, "/tmp/gh-aw/test.log") + require.NotEmpty(t, execSteps) + execContent := strings.Join(execSteps[len(execSteps)-1], "\n") + assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) + }) + } t.Run("non-microVM runtimes keep the host install only", func(t *testing.T) { defaultWorkflow := &WorkflowData{