diff --git a/.github/workflows/daily-awf-spec-compiler-surfacing.lock.yml b/.github/workflows/daily-awf-spec-compiler-surfacing.lock.yml index 2b4514dc147..fab7b240c43 100644 --- a/.github/workflows/daily-awf-spec-compiler-surfacing.lock.yml +++ b/.github/workflows/daily-awf-spec-compiler-surfacing.lock.yml @@ -522,6 +522,11 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.1 - name: Install Pi CLI run: npm install --ignore-scripts -g @earendil-works/pi-coding-agent@0.84.2 + - name: Install Pi CLI in docker-sbx path + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/engine-cli/bin" + npm install --ignore-scripts --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @earendil-works/pi-coding-agent@0.84.2 + ln -sf "../node_modules/.bin/pi" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/pi" - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -852,7 +857,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" && : "${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 && cd "${GITHUB_WORKSPACE}" && mkdir -p /tmp/gh-aw/pi-agent-dir && printf '\''%s\n'\'' '\''{"providers":{"aw-gateway":{"api":"openai-completions","apiKey":"COPILOT_GITHUB_TOKEN","baseUrl":"http://api-proxy:10002","models":[{"id":"gpt-5.4"}]}}}'\'' > /tmp/gh-aw/pi-agent-dir/models.json && cat /tmp/gh-aw/aw-prompts/prompt.txt | pi --print --mode json --no-session --model aw-gateway/gpt-5.4 --extension "${RUNNER_TEMP}/gh-aw/actions/pi_provider.cjs" --extension "${RUNNER_TEMP}/gh-aw/actions/pi_steering_extension.cjs" 2>&1 | tee /tmp/gh-aw/pi-streaming.jsonl' 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" && : "${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 && cd "${GITHUB_WORKSPACE}" && mkdir -p /tmp/gh-aw/pi-agent-dir && printf '\''%s\n'\'' '\''{"providers":{"aw-gateway":{"api":"openai-completions","apiKey":"COPILOT_GITHUB_TOKEN","baseUrl":"http://api-proxy:10002","models":[{"id":"gpt-5.4"}]}}}'\'' > /tmp/gh-aw/pi-agent-dir/models.json && cat /tmp/gh-aw/aw-prompts/prompt.txt | pi --print --mode json --no-session --model aw-gateway/gpt-5.4 --extension "${RUNNER_TEMP}/gh-aw/actions/pi_provider.cjs" --extension "${RUNNER_TEMP}/gh-aw/actions/pi_steering_extension.cjs" 2>&1 | tee /tmp/gh-aw/pi-streaming.jsonl' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index ea95fbad8f8..7bb5f8d1426 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -569,6 +569,11 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.1 - name: Install Pi CLI run: npm install --ignore-scripts -g @earendil-works/pi-coding-agent@0.84.2 + - name: Install Pi CLI in docker-sbx path + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/engine-cli/bin" + npm install --ignore-scripts --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @earendil-works/pi-coding-agent@0.84.2 + ln -sf "../node_modules/.bin/pi" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/pi" - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -906,7 +911,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" && : "${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 && cd "${GITHUB_WORKSPACE}" && mkdir -p /tmp/gh-aw/pi-agent-dir && printf '\''%s\n'\'' '\''{"providers":{"aw-gateway":{"api":"openai-completions","apiKey":"COPILOT_GITHUB_TOKEN","baseUrl":"http://api-proxy:10002","models":[{"id":"gpt-5.4"}]}}}'\'' > /tmp/gh-aw/pi-agent-dir/models.json && cat /tmp/gh-aw/aw-prompts/prompt.txt | pi --print --mode json --no-session --model aw-gateway/gpt-5.4 --extension "${RUNNER_TEMP}/gh-aw/actions/pi_provider.cjs" --extension "${RUNNER_TEMP}/gh-aw/actions/pi_steering_extension.cjs" 2>&1 | tee /tmp/gh-aw/pi-streaming.jsonl' 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" && : "${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 && cd "${GITHUB_WORKSPACE}" && mkdir -p /tmp/gh-aw/pi-agent-dir && printf '\''%s\n'\'' '\''{"providers":{"aw-gateway":{"api":"openai-completions","apiKey":"COPILOT_GITHUB_TOKEN","baseUrl":"http://api-proxy:10002","models":[{"id":"gpt-5.4"}]}}}'\'' > /tmp/gh-aw/pi-agent-dir/models.json && cat /tmp/gh-aw/aw-prompts/prompt.txt | pi --print --mode json --no-session --model aw-gateway/gpt-5.4 --extension "${RUNNER_TEMP}/gh-aw/actions/pi_provider.cjs" --extension "${RUNNER_TEMP}/gh-aw/actions/pi_steering_extension.cjs" 2>&1 | tee /tmp/gh-aw/pi-streaming.jsonl' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/pkg/workflow/docker_sbx_test.go b/pkg/workflow/docker_sbx_test.go index e7f74818269..9efaa4c45ba 100644 --- a/pkg/workflow/docker_sbx_test.go +++ b/pkg/workflow/docker_sbx_test.go @@ -353,6 +353,40 @@ func TestDockerSbxEngineCLIWiring(t *testing.T) { execContent := strings.Join(execSteps[0], "\n") assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) }) + + t.Run("pi install and execution use sbx-visible CLI path", func(t *testing.T) { + engine := NewPiEngine() + workflowData.EngineConfig = &EngineConfig{ID: "pi"} + installSteps := engine.GetInstallationSteps(workflowData) + installContent := strings.Join(flattenSteps(installSteps), "\n") + assert.Contains(t, installContent, `npm install --ignore-scripts --prefix "${RUNNER_TEMP}/gh-aw/engine-cli" @earendil-works/pi-coding-agent@`+string(constants.DefaultPiVersion)) + assert.Contains(t, installContent, `ln -sf "../node_modules/.bin/pi" "${RUNNER_TEMP}/gh-aw/engine-cli/bin/pi"`) + + execSteps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") + require.NotEmpty(t, execSteps) + execContent := strings.Join(execSteps[0], "\n") + assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) + }) + + t.Run("pi execution uses sbx-visible CLI path even with firewall disabled", func(t *testing.T) { + // sandbox.agent.runtime: docker-sbx can be configured alongside an explicit + // network.firewall: false, which takes the non-AWF execution path. The + // staged CLI PATH export must still be present so the microVM can see `pi`. + nonFirewallWorkflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ID: "pi"}, + SandboxConfig: &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: AgentRuntimeDockerSbx, SudoExplicitlyEnabled: true}}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: false}, + }, + } + + engine := NewPiEngine() + execSteps := engine.GetExecutionSteps(nonFirewallWorkflowData, "/tmp/gh-aw/test.log") + require.NotEmpty(t, execSteps) + execContent := strings.Join(execSteps[0], "\n") + assert.Contains(t, execContent, `export PATH="${RUNNER_TEMP}/gh-aw/engine-cli/bin:$PATH"`) + }) } // flattenSteps joins a small slice of GitHubActionStep values so docker-sbx tests can diff --git a/pkg/workflow/pi_engine.go b/pkg/workflow/pi_engine.go index c6013ba2d0b..df03935a67f 100644 --- a/pkg/workflow/pi_engine.go +++ b/pkg/workflow/pi_engine.go @@ -233,6 +233,19 @@ func (e *PiEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActi }, ) + // microVM runtimes (docker-sbx/cloud-hypervisor) cannot see the globally installed + // CLI in the hosted tool cache, so stage a second copy under RUNNER_TEMP. + if isDockerSbxRuntime(workflowData) || isCloudHypervisorRuntime(workflowData) { + npmSteps = append(npmSteps, GenerateDockerSbxNpmCLIInstallStep( + "@earendil-works/pi-coding-agent", + version, + "Install Pi CLI in docker-sbx path", + "pi", + false, + false, + )) + } + steps := BuildNpmEngineInstallStepsWithAWF(npmSteps, workflowData) // Install extensions declared in engine.extensions: [...] @@ -369,6 +382,9 @@ func (e *PiEngine) buildPiCommand(workflowData *WorkflowData, commandName string func (e *PiEngine) buildPiExecutionCommand(workflowData *WorkflowData, logFile, piCommand string, firewallEnabled, modelConfigured bool, profile universalLLMBackendProfile) string { if firewallEnabled { piCommandWithPath := fmt.Sprintf("%s && %s", GetNpmBinPathSetup(), piCommand) + if dockerSbxCLIPath := GetDockerSbxNpmCLIPathSetup(workflowData); dockerSbxCLIPath != "" { + piCommandWithPath = fmt.Sprintf("%s && %s", dockerSbxCLIPath, piCommandWithPath) + } if mcpCLIPath := GetMCPCLIPathSetup(workflowData); mcpCLIPath != "" { piCommandWithPath = fmt.Sprintf("%s && %s", mcpCLIPath, piCommandWithPath) } @@ -386,6 +402,12 @@ func (e *PiEngine) buildPiExecutionCommand(workflowData *WorkflowData, logFile, ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, profile.coreSecretNames), }) } + // Even without AWF, a docker-sbx/cloud-hypervisor runtime can be configured + // (e.g. network.firewall: false paired with sandbox.agent.runtime: docker-sbx), + // so the staged CLI path must still be exported for the microVM to see `pi`. + if dockerSbxCLIPath := GetDockerSbxNpmCLIPathSetup(workflowData); dockerSbxCLIPath != "" { + piCommand = fmt.Sprintf("%s && %s", dockerSbxCLIPath, piCommand) + } return fmt.Sprintf(`set -o pipefail printf '%%s' "$(date +%%s%%3N)" > %s touch %s