diff --git a/.github/workflows/build-test-rust.lock.yml b/.github/workflows/build-test-rust.lock.yml index 496ad312..5dc3b74d 100644 --- a/.github/workflows/build-test-rust.lock.yml +++ b/.github/workflows/build-test-rust.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.43.13). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -21,7 +21,7 @@ # # Build Test Rust # -# frontmatter-hash: ce653278c404e9224b068baacf3bdd06b6735c2759288f0faf0121c7381f3b53 +# frontmatter-hash: abe6a719756fbac074aec027963912d0b0ac8782c539c40df8bff1803473182b name: "Build Test Rust" "on": @@ -54,11 +54,11 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.13 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_WORKFLOW_FILE: "build-test-rust.lock.yml" with: @@ -93,12 +93,13 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.13 with: destination: /opt/gh-aw/actions - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + fetch-depth: 1 persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh @@ -117,7 +118,7 @@ jobs: id: checkout-pr if: | github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -127,13 +128,58 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.407", + cli_version: "v0.43.13", + workflow_name: "Build Test Rust", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults","github","rust","crates.io"], + firewall_enabled: true, + awf_version: "v0.16.0", + awmg_version: "", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.407 - name: Install awf dependencies run: npm ci - name: Build awf @@ -170,16 +216,16 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.16.0 ghcr.io/github/gh-aw-firewall/squid:0.16.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' {"add_comment":{"max":1},"add_labels":{"allowed":["build-test-rust"],"max":3},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ { "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", @@ -293,8 +339,8 @@ jobs: "name": "missing_data" } ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' { "add_comment": { "defaultMax": 1, @@ -358,18 +404,17 @@ jobs: } } } - EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config run: | # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" + # Mask immediately to prevent timing vulnerabilities API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs echo "::add-mask::${API_KEY}" + PORT=3001 + # Set outputs for next steps { echo "safe_outputs_api_key=${API_KEY}" @@ -413,20 +458,18 @@ jobs: # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="80" export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export DEBUG="*" - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.0.113' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' mkdir -p /home/runner/.copilot - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -454,54 +497,9 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.42.17", - workflow_name: "Build Test Rust", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults","github","rust"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); + GH_AW_MCP_CONFIG_EOF - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); @@ -520,12 +518,12 @@ jobs: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} run: | bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - PROMPT_EOF + GH_AW_PROMPT_EOF cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" GitHub API Access Instructions @@ -569,15 +567,15 @@ jobs: {{/if}} - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" {{#runtime-import .github/workflows/build-test-rust.md}} - PROMPT_EOF + GH_AW_PROMPT_EOF - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -607,7 +605,7 @@ jobs: } }); - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt with: @@ -624,6 +622,8 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -645,6 +645,17 @@ jobs: GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -673,7 +684,7 @@ jobs: bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -695,7 +706,7 @@ jobs: if-no-files-found: warn - name: Ingest agent output id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,index.crates.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sh.rustup.rs,static.crates.io,static.rust-lang.org,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" @@ -724,7 +735,7 @@ jobs: if-no-files-found: ignore - name: Parse agent logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -735,7 +746,7 @@ jobs: await main(); - name: Parse MCP gateway logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -786,20 +797,9 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.13 with: destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 @@ -813,7 +813,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process No-Op Messages id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: 1 @@ -827,7 +827,7 @@ jobs: await main(); - name: Record Missing Tool id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Build Test Rust" @@ -840,12 +840,13 @@ jobs: await main(); - name: Handle Agent Failure id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Build Test Rust" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "build-test-rust" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"runFailure\":\"**Build Test Failed** [{workflow_name}]({run_url}) - See logs for details\"}" @@ -858,7 +859,7 @@ jobs: await main(); - name: Handle No-Op Message id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Build Test Rust" @@ -875,7 +876,7 @@ jobs: await main(); - name: Update reaction comment with completion status id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} @@ -903,7 +904,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.13 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -924,7 +925,7 @@ jobs: run: | echo "Agent output-types: $AGENT_OUTPUT_TYPES" - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: WORKFLOW_NAME: "Build Test Rust" WORKFLOW_DESCRIPTION: "Build Test Rust" @@ -945,7 +946,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.407 - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -977,7 +978,7 @@ jobs: XDG_CONFIG_HOME: /home/runner - name: Parse threat detection results id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -1016,7 +1017,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.13 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -1032,7 +1033,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"build-test-rust\"]},\"missing_data\":{},\"missing_tool\":{}}" diff --git a/.github/workflows/build-test-rust.md b/.github/workflows/build-test-rust.md index 758ea98b..5a5f8895 100644 --- a/.github/workflows/build-test-rust.md +++ b/.github/workflows/build-test-rust.md @@ -19,6 +19,7 @@ network: - defaults - github - rust + - crates.io tools: bash: - "*" diff --git a/.github/workflows/test-chroot.yml b/.github/workflows/test-chroot.yml index 2f5457f5..b3ea6e4d 100644 --- a/.github/workflows/test-chroot.yml +++ b/.github/workflows/test-chroot.yml @@ -157,6 +157,8 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable - name: Setup Java uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v4 @@ -186,6 +188,12 @@ jobs: echo "Captured CARGO_HOME: ${CARGO_HOME}" fi + # Rust: RUSTUP_HOME is needed so rustc can find the toolchain + if [ -n "$RUSTUP_HOME" ]; then + echo "RUSTUP_HOME=${RUSTUP_HOME}" >> $GITHUB_ENV + echo "Captured RUSTUP_HOME: ${RUSTUP_HOME}" + fi + # Java: JAVA_HOME is needed so entrypoint can add $JAVA_HOME/bin to PATH # The setup-java action sets JAVA_HOME but sudo may not preserve it if [ -n "$JAVA_HOME" ]; then @@ -210,11 +218,14 @@ jobs: echo "GOROOT: $GOROOT" echo "Ruby: $(ruby --version)" echo "Gem: $(gem --version)" + echo "Bundler: $(bundle --version 2>&1 || echo 'Not installed')" echo "Rust: $(rustc --version)" echo "Cargo: $(cargo --version)" echo "CARGO_HOME: $CARGO_HOME" + echo "RUSTUP_HOME: $RUSTUP_HOME" echo "Java: $(java --version 2>&1 | head -1)" echo "JAVA_HOME: $JAVA_HOME" + echo "Maven: $(mvn --version 2>&1 | head -1 || echo 'Not installed')" echo "dotnet: $(dotnet --version 2>&1)" echo "DOTNET_ROOT: $DOTNET_ROOT" diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 6a74a155..65ba8cc5 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -4,7 +4,24 @@ # - ghcr.io/catthehacker/ubuntu:runner-22.04: Closer to GitHub Actions runner (~2-5GB) # - ghcr.io/catthehacker/ubuntu:full-22.04: Near-identical to GitHub Actions runner (~20GB compressed) # Use --build-arg BASE_IMAGE= to customize +# NOTE: ARG declared before first FROM is global and available in all FROM statements ARG BASE_IMAGE=ubuntu:22.04 + +# Multi-stage build: Use official Rust image to build one-shot-token library +# SECURITY: Using official rust:1.77-slim image prevents executing unverified +# scripts from the internet during build time (supply chain attack mitigation) +# NOTE: Rust 1.77+ required for C string literal syntax (c"...") used in src/lib.rs +FROM rust:1.77-slim AS rust-builder + +# Copy one-shot-token source files +COPY one-shot-token/Cargo.toml /tmp/one-shot-token/Cargo.toml +COPY one-shot-token/src/ /tmp/one-shot-token/src/ + +# Build the one-shot-token library +WORKDIR /tmp/one-shot-token +RUN cargo build --release + +# Main stage FROM ${BASE_IMAGE} # Install required packages and Node.js 22 @@ -66,20 +83,11 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY pid-logger.sh /usr/local/bin/pid-logger.sh RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh -# Build one-shot-token LD_PRELOAD library for single-use token access +# Copy pre-built one-shot-token library from rust-builder stage # This prevents tokens from being read multiple times (e.g., by malicious code) -COPY one-shot-token/one-shot-token.c /tmp/one-shot-token.c -RUN set -eux; \ - BUILD_PKGS="gcc libc6-dev"; \ - apt-get update && \ - ( apt-get install -y --no-install-recommends $BUILD_PKGS || \ - (rm -rf /var/lib/apt/lists/* && apt-get update && \ - apt-get install -y --no-install-recommends $BUILD_PKGS) ) && \ - gcc -shared -fPIC -O2 -Wall -o /usr/local/lib/one-shot-token.so /tmp/one-shot-token.c -ldl -lpthread && \ - rm /tmp/one-shot-token.c && \ - apt-get remove -y $BUILD_PKGS && \ - apt-get autoremove -y && \ - rm -rf /var/lib/apt/lists/* +# SECURITY: Using multi-stage build with official Rust image avoids executing +# unverified scripts from the internet during build time +COPY --from=rust-builder /tmp/one-shot-token/target/release/libone_shot_token.so /usr/local/lib/one-shot-token.so # Install Docker stub script that shows helpful error message # Docker-in-Docker support was removed in v0.9.1 diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 9a3ab69f..eced7d04 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -292,6 +292,16 @@ AWFEOF echo "[entrypoint] Adding CARGO_HOME/bin to PATH: ${AWF_CARGO_HOME}/bin" echo "export PATH=\"${AWF_CARGO_HOME}/bin:\$PATH\"" >> "/host${SCRIPT_FILE}" echo "export CARGO_HOME=\"${AWF_CARGO_HOME}\"" >> "/host${SCRIPT_FILE}" + # Also set RUSTUP_HOME if provided (needed for rustc to find toolchain) + if [ -n "${AWF_RUSTUP_HOME}" ]; then + echo "[entrypoint] Setting RUSTUP_HOME: ${AWF_RUSTUP_HOME}" + echo "export RUSTUP_HOME=\"${AWF_RUSTUP_HOME}\"" >> "/host${SCRIPT_FILE}" + fi + else + # Fallback: detect Cargo from default location if CARGO_HOME not provided + # This ensures Rust binaries work even when CARGO_HOME env var is not set + echo "# Add Cargo bin for Rust if it exists (fallback when CARGO_HOME not provided)" >> "/host${SCRIPT_FILE}" + echo "[ -d \"\$HOME/.cargo/bin\" ] && export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> "/host${SCRIPT_FILE}" fi # Add JAVA_HOME/bin to PATH if provided (for Java on GitHub Actions) # Also set LD_LIBRARY_PATH to include Java's lib directory for libjli.so diff --git a/containers/agent/one-shot-token/.gitignore b/containers/agent/one-shot-token/.gitignore index 140f8cf8..f3dee79b 100644 --- a/containers/agent/one-shot-token/.gitignore +++ b/containers/agent/one-shot-token/.gitignore @@ -1 +1,9 @@ +# Build output *.so + +# Rust build artifacts +target/ +Cargo.lock + +# C build artifacts (legacy) +*.o diff --git a/containers/agent/one-shot-token/Cargo.toml b/containers/agent/one-shot-token/Cargo.toml new file mode 100644 index 00000000..9d8093bc --- /dev/null +++ b/containers/agent/one-shot-token/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "one-shot-token" +version = "0.1.0" +edition = "2021" +description = "LD_PRELOAD library for one-shot access to sensitive environment variables" +license = "MIT" + +[lib] +name = "one_shot_token" +crate-type = ["cdylib"] + +[dependencies] +libc = "0.2" +once_cell = "1.19" + +[profile.release] +opt-level = 2 +lto = true +strip = true diff --git a/containers/agent/one-shot-token/README.md b/containers/agent/one-shot-token/README.md index db2d21c0..eb9cb642 100644 --- a/containers/agent/one-shot-token/README.md +++ b/containers/agent/one-shot-token/README.md @@ -158,22 +158,22 @@ In chroot mode, the library must be accessible from within the chroot (host file ### In Docker (automatic) -The Dockerfile compiles the library during image build: +The Dockerfile compiles the Rust library during image build: ```dockerfile -RUN gcc -shared -fPIC -O2 -Wall \ - -o /usr/local/lib/one-shot-token.so \ - /tmp/one-shot-token.c \ - -ldl -lpthread +RUN cargo build --release && \ + cp target/release/libone_shot_token.so /usr/local/lib/one-shot-token.so ``` ### Locally (for testing) +Requires Rust toolchain (install via [rustup](https://rustup.rs/)): + ```bash ./build.sh ``` -This produces `one-shot-token.so` in the current directory. +This builds `target/release/libone_shot_token.so` and creates a symlink `one-shot-token.so` for backwards compatibility. ## Testing @@ -274,6 +274,20 @@ Note: The `AWF_ONE_SHOT_TOKENS` variable must be exported before running `awf` s - **In-process getenv() calls**: Since values are cached, any code in the same process can still call `getenv()` and get the cached token - **Static linking**: Programs statically linked with libc bypass LD_PRELOAD - **Direct syscalls**: Code that reads `/proc/self/environ` directly (without getenv) bypasses this protection +- **Task-level /proc exposure**: `/proc/PID/task/TID/environ` may still expose tokens even after `unsetenv()`. The library checks and logs warnings about this exposure. + +### Environment Verification + +After calling `unsetenv()` to clear tokens, the library automatically verifies whether the token was successfully removed by directly checking the process's environment pointer. This works correctly in both regular and chroot modes. + +**Log messages:** +- `INFO: Token cleared from process environment` - Token successfully cleared (✓ secure) +- `WARNING: Token still exposed in process environment` - Token still visible (⚠ security concern) +- `INFO: Token cleared (environ is null)` - Environment pointer is null + +This verification runs automatically after `unsetenv()` on first access to each sensitive token and helps identify potential security issues with environment exposure. + +**Note on chroot mode:** The verification uses the process's `environ` pointer directly rather than reading from `/proc/self/environ`. This is necessary because in chroot mode, `/proc` may be bind-mounted from the host and show stale environment data. ### Defense in Depth @@ -285,12 +299,13 @@ This library is one layer in AWF's security model: ## Limitations -- **x86_64 Linux only**: The library is compiled for x86_64 Ubuntu +- **Linux only**: The library is compiled for Linux (x86_64 and potentially other architectures via Rust cross-compilation) - **glibc programs only**: Programs using musl libc or statically linked programs are not affected - **Single process**: Child processes inherit the LD_PRELOAD but have their own token state and cache (each starts fresh) ## Files -- `one-shot-token.c` - Library source code +- `src/lib.rs` - Library source code (Rust) +- `Cargo.toml` - Rust package configuration - `build.sh` - Local build script - `README.md` - This documentation diff --git a/containers/agent/one-shot-token/build.sh b/containers/agent/one-shot-token/build.sh index 79227f8f..6996725c 100644 --- a/containers/agent/one-shot-token/build.sh +++ b/containers/agent/one-shot-token/build.sh @@ -1,34 +1,39 @@ #!/bin/bash # Build the one-shot-token LD_PRELOAD library -# This script compiles the shared library for x86_64 Ubuntu +# This script compiles the Rust shared library set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SOURCE_FILE="${SCRIPT_DIR}/one-shot-token.c" -OUTPUT_FILE="${SCRIPT_DIR}/one-shot-token.so" - -echo "[build] Compiling one-shot-token.so..." - -# Compile as a shared library with position-independent code -# -shared: create a shared library -# -fPIC: position-independent code (required for shared libs) -# -ldl: link with libdl for dlsym -# -lpthread: link with pthread for mutex -# -O2: optimize for performance -# -Wall -Wextra: enable warnings -gcc -shared -fPIC \ - -O2 -Wall -Wextra \ - -o "${OUTPUT_FILE}" \ - "${SOURCE_FILE}" \ - -ldl -lpthread - -echo "[build] Successfully built: ${OUTPUT_FILE}" +LINK_FILE="${SCRIPT_DIR}/one-shot-token.so" + +echo "[build] Building one-shot-token with Cargo..." + +cd "${SCRIPT_DIR}" + +# Build the release version +cargo build --release + +# Determine the output file based on platform +if [[ "$(uname)" == "Darwin" ]]; then + OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.dylib" + echo "[build] Successfully built: ${OUTPUT_FILE} (macOS)" +else + OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.so" + echo "[build] Successfully built: ${OUTPUT_FILE}" + + # Create symlink for backwards compatibility (Linux only) + if [[ -L "${LINK_FILE}" ]]; then + rm "${LINK_FILE}" + fi + ln -sf "target/release/libone_shot_token.so" "${LINK_FILE}" + echo "[build] Created symlink: ${LINK_FILE} -> target/release/libone_shot_token.so" +fi # Verify it's a valid shared library -if file "${OUTPUT_FILE}" | grep -q "shared object"; then - echo "[build] Verified: valid shared object" +if file "${OUTPUT_FILE}" | grep -qE "shared object|dynamically linked"; then + echo "[build] Verified: valid shared library" else - echo "[build] ERROR: Output is not a valid shared object" + echo "[build] ERROR: Output is not a valid shared library" exit 1 fi diff --git a/containers/agent/one-shot-token/src/lib.rs b/containers/agent/one-shot-token/src/lib.rs new file mode 100644 index 00000000..1472c5fb --- /dev/null +++ b/containers/agent/one-shot-token/src/lib.rs @@ -0,0 +1,403 @@ +//! One-Shot Token LD_PRELOAD Library +//! +//! Intercepts getenv() calls for sensitive token environment variables. +//! On first access, caches the value in memory and unsets from environment. +//! Subsequent calls return the cached value, so the process can read tokens +//! multiple times while /proc/self/environ no longer exposes them. +//! +//! Configuration: +//! AWF_ONE_SHOT_TOKENS - Comma-separated list of token names to protect +//! If not set, uses built-in defaults +//! +//! Compile: cargo build --release +//! Usage: LD_PRELOAD=/path/to/libone_shot_token.so ./your-program + +use libc::{c_char, c_void}; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::ptr; +use std::sync::Mutex; + +// External declaration of the environ pointer +// This is a POSIX standard global that points to the process's environment +extern "C" { + static mut environ: *mut *mut c_char; +} + +/// Maximum number of tokens we can track +const MAX_TOKENS: usize = 100; + +/// Default sensitive token environment variable names +const DEFAULT_SENSITIVE_TOKENS: &[&str] = &[ + // GitHub tokens + "COPILOT_GITHUB_TOKEN", + "GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_API_TOKEN", + "GITHUB_PAT", + "GH_ACCESS_TOKEN", + // OpenAI tokens + "OPENAI_API_KEY", + "OPENAI_KEY", + // Anthropic/Claude tokens + "ANTHROPIC_API_KEY", + "CLAUDE_API_KEY", + // Codex tokens + "CODEX_API_KEY", +]; + +/// State for tracking tokens and their cached values +struct TokenState { + /// List of sensitive token names to protect + tokens: Vec, + /// Cached token values - stored on first access so subsequent reads succeed + /// even after the variable is unset from the environment. This allows + /// /proc/self/environ to be cleaned while the process can still read tokens. + /// Maps token name to cached C string pointer (or null if token was not set). + cache: HashMap, + /// Whether initialization has completed + initialized: bool, +} + +// SAFETY: TokenState is only accessed through a Mutex, ensuring thread safety +unsafe impl Send for TokenState {} +unsafe impl Sync for TokenState {} + +impl TokenState { + fn new() -> Self { + Self { + tokens: Vec::new(), + cache: HashMap::new(), + initialized: false, + } + } +} + +/// Global state protected by a mutex +static STATE: Lazy> = Lazy::new(|| Mutex::new(TokenState::new())); + +/// Type alias for the real getenv function +type GetenvFn = unsafe extern "C" fn(*const c_char) -> *mut c_char; + +/// Cached pointer to the real getenv function +static REAL_GETENV: Lazy = Lazy::new(|| { + // SAFETY: We're looking up a standard C library function + unsafe { + let symbol = libc::dlsym(libc::RTLD_NEXT, c"getenv".as_ptr()); + if symbol.is_null() { + eprintln!("[one-shot-token] FATAL: Could not find real getenv"); + std::process::abort(); + } + std::mem::transmute::<*mut c_void, GetenvFn>(symbol) + } +}); + +/// Cached pointer to the real secure_getenv function (may be null if unavailable) +static REAL_SECURE_GETENV: Lazy> = Lazy::new(|| { + // SAFETY: We're looking up a standard C library function + unsafe { + let symbol = libc::dlsym(libc::RTLD_NEXT, c"secure_getenv".as_ptr()); + if symbol.is_null() { + eprintln!("[one-shot-token] WARNING: secure_getenv not available, falling back to getenv"); + None + } else { + Some(std::mem::transmute::<*mut c_void, GetenvFn>(symbol)) + } + } +}); + +/// Call the real getenv function +/// +/// # Safety +/// The `name` parameter must be a valid null-terminated C string +unsafe fn call_real_getenv(name: *const c_char) -> *mut c_char { + (*REAL_GETENV)(name) +} + +/// Call the real secure_getenv function, falling back to getenv if unavailable +/// +/// # Safety +/// The `name` parameter must be a valid null-terminated C string +unsafe fn call_real_secure_getenv(name: *const c_char) -> *mut c_char { + match *REAL_SECURE_GETENV { + Some(func) => func(name), + None => call_real_getenv(name), + } +} + +/// Initialize the token list from AWF_ONE_SHOT_TOKENS or defaults +/// +/// # Safety +/// Must be called with STATE lock held +fn init_token_list(state: &mut TokenState) { + if state.initialized { + return; + } + + // Get configuration from environment + let config_cstr = CString::new("AWF_ONE_SHOT_TOKENS").unwrap(); + // SAFETY: We're calling the real getenv with a valid C string + let config_ptr = unsafe { call_real_getenv(config_cstr.as_ptr()) }; + + if !config_ptr.is_null() { + // SAFETY: config_ptr is valid if not null + let config = unsafe { CStr::from_ptr(config_ptr) }; + if let Ok(config_str) = config.to_str() { + if !config_str.is_empty() { + // Parse comma-separated token list + for token in config_str.split(',') { + let token = token.trim(); + if !token.is_empty() && state.tokens.len() < MAX_TOKENS { + state.tokens.push(token.to_string()); + } + } + + if !state.tokens.is_empty() { + eprintln!( + "[one-shot-token] Initialized with {} custom token(s) from AWF_ONE_SHOT_TOKENS", + state.tokens.len() + ); + state.initialized = true; + return; + } + + // Config was set but parsed to zero tokens - fall back to defaults + eprintln!("[one-shot-token] WARNING: AWF_ONE_SHOT_TOKENS was set but parsed to zero tokens"); + eprintln!("[one-shot-token] WARNING: Falling back to default token list to maintain protection"); + } + } + } + + // Use default token list + for token in DEFAULT_SENSITIVE_TOKENS { + if state.tokens.len() >= MAX_TOKENS { + break; + } + state.tokens.push((*token).to_string()); + } + + eprintln!( + "[one-shot-token] Initialized with {} default token(s)", + state.tokens.len() + ); + state.initialized = true; +} + +/// Check if a token name is sensitive +fn is_sensitive_token(state: &TokenState, name: &str) -> bool { + state.tokens.iter().any(|t| t == name) +} + +/// Format token value for logging: show first 4 characters + "..." +fn format_token_value(value: &str) -> String { + if value.is_empty() { + return "(empty)".to_string(); + } + + if value.len() <= 4 { + format!("{}...", value) + } else { + format!("{}...", &value[..4]) + } +} + +/// Check if a token still exists in the process environment +/// +/// This function verifies whether unsetenv() successfully cleared the token +/// by directly checking the process's environ pointer. This works correctly +/// in both chroot and non-chroot modes (reading /proc/self/environ fails in +/// chroot because it shows the host's procfs, not the chrooted process's state). +fn check_task_environ_exposure(token_name: &str) { + // SAFETY: environ is a standard POSIX global that points to the process's environment. + // It's safe to read as long as we don't hold references across modifications. + // We're only reading it after unsetenv() has completed, so the pointer is stable. + unsafe { + let mut env_ptr = environ; + if env_ptr.is_null() { + eprintln!("[one-shot-token] INFO: Token {} cleared (environ is null)", token_name); + return; + } + + // Iterate through environment variables + let token_prefix = format!("{}=", token_name); + let token_prefix_bytes = token_prefix.as_bytes(); + + while !(*env_ptr).is_null() { + let env_cstr = CStr::from_ptr(*env_ptr); + let env_bytes = env_cstr.to_bytes(); + + // Check if this entry starts with our token name + if env_bytes.len() >= token_prefix_bytes.len() + && &env_bytes[..token_prefix_bytes.len()] == token_prefix_bytes { + eprintln!( + "[one-shot-token] WARNING: Token {} still exposed in process environment", + token_name + ); + return; + } + + env_ptr = env_ptr.add(1); + } + + // Token not found in environment - success! + eprintln!( + "[one-shot-token] INFO: Token {} cleared from process environment", + token_name + ); + } +} + +/// Core implementation for cached token access +/// +/// # Safety +/// - `name` must be a valid null-terminated C string +/// - `real_getenv_fn` must be a valid function to call for getting the real value +unsafe fn handle_getenv_impl( + name: *const c_char, + real_getenv_fn: unsafe fn(*const c_char) -> *mut c_char, + via_secure: bool, +) -> *mut c_char { + // Null name - pass through + if name.is_null() { + return real_getenv_fn(name); + } + + // Convert name to Rust string for comparison + let name_cstr = CStr::from_ptr(name); + let name_str = match name_cstr.to_str() { + Ok(s) => s, + Err(_) => return real_getenv_fn(name), + }; + + // Lock state and ensure initialization + let mut state = match STATE.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + if !state.initialized { + init_token_list(&mut state); + } + + // Check if this is a sensitive token + if !is_sensitive_token(&state, name_str) { + // Not sensitive - pass through (drop lock first for performance) + drop(state); + return real_getenv_fn(name); + } + + // Sensitive token - check if already cached + if let Some(&cached_ptr) = state.cache.get(name_str) { + // Already accessed - return cached value (may be null if token wasn't set) + return cached_ptr; + } + + // First access - get the real value and cache it + let result = real_getenv_fn(name); + + if result.is_null() { + // Token not set - cache null to prevent repeated log messages + state.cache.insert(name_str.to_string(), ptr::null_mut()); + return ptr::null_mut(); + } + + // Copy the value before unsetting + let value_cstr = CStr::from_ptr(result); + let value_str = value_cstr.to_str().unwrap_or(""); + let value_bytes = value_cstr.to_bytes_with_nul(); + + // Allocate memory that will never be freed (must persist for caller's use) + let cached = libc::malloc(value_bytes.len()) as *mut c_char; + if cached.is_null() { + eprintln!("[one-shot-token] ERROR: Failed to allocate memory for token value"); + std::process::abort(); + } + + // Copy the value + ptr::copy_nonoverlapping(value_bytes.as_ptr(), cached as *mut u8, value_bytes.len()); + + // Cache the pointer so subsequent reads return the same value + state.cache.insert(name_str.to_string(), cached); + + // Unset the environment variable so it's no longer accessible + libc::unsetenv(name); + + // Verify the token was cleared from the process environment + check_task_environ_exposure(name_str); + + let suffix = if via_secure { " (via secure_getenv)" } else { "" }; + eprintln!( + "[one-shot-token] Token {} accessed and cached (value: {}){}", + name_str, format_token_value(value_str), suffix + ); + + cached +} + +/// Intercepted getenv function +/// +/// For sensitive tokens: +/// - First call: caches the value, unsets from environment, returns cached value +/// - Subsequent calls: returns the cached value from memory +/// +/// This clears tokens from /proc/self/environ while allowing the process +/// to read them multiple times via getenv(). +/// +/// For all other variables: passes through to real getenv +/// +/// # Safety +/// This function is called from C code and must maintain C ABI compatibility. +/// The `name` parameter must be a valid null-terminated C string. +#[no_mangle] +pub unsafe extern "C" fn getenv(name: *const c_char) -> *mut c_char { + handle_getenv_impl(name, call_real_getenv, false) +} + +/// Intercepted secure_getenv function +/// +/// This function preserves secure_getenv semantics (returns NULL in privileged contexts) +/// while applying the same cached token protection as getenv. +/// +/// For sensitive tokens: +/// - First call: caches the value, unsets from environment, returns cached value +/// - Subsequent calls: returns the cached value from memory +/// +/// For all other variables: passes through to real secure_getenv (or getenv if unavailable) +/// +/// # Safety +/// This function is called from C code and must maintain C ABI compatibility. +/// The `name` parameter must be a valid null-terminated C string. +#[no_mangle] +pub unsafe extern "C" fn secure_getenv(name: *const c_char) -> *mut c_char { + handle_getenv_impl(name, call_real_secure_getenv, true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_tokens_defined() { + assert!(!DEFAULT_SENSITIVE_TOKENS.is_empty()); + assert!(DEFAULT_SENSITIVE_TOKENS.contains(&"GITHUB_TOKEN")); + assert!(DEFAULT_SENSITIVE_TOKENS.contains(&"OPENAI_API_KEY")); + } + + #[test] + fn test_token_state_new() { + let state = TokenState::new(); + assert!(state.tokens.is_empty()); + assert!(state.cache.is_empty()); + assert!(!state.initialized); + } + + #[test] + fn test_format_token_value() { + assert_eq!(format_token_value(""), "(empty)"); + assert_eq!(format_token_value("ab"), "ab..."); + assert_eq!(format_token_value("abcd"), "abcd..."); + assert_eq!(format_token_value("abcde"), "abcd..."); + assert_eq!(format_token_value("ghp_1234567890"), "ghp_..."); + } +} diff --git a/src/docker-manager.ts b/src/docker-manager.ts index bdd04717..e32023a9 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -358,6 +358,10 @@ export function generateDockerCompose( if (process.env.CARGO_HOME) { environment.AWF_CARGO_HOME = process.env.CARGO_HOME; } + // Rust: Pass RUSTUP_HOME so rustc/cargo can find the toolchain + if (process.env.RUSTUP_HOME) { + environment.AWF_RUSTUP_HOME = process.env.RUSTUP_HOME; + } // Java: Pass JAVA_HOME so entrypoint can add $JAVA_HOME/bin to PATH and set JAVA_HOME if (process.env.JAVA_HOME) { environment.AWF_JAVA_HOME = process.env.JAVA_HOME; diff --git a/tests/fixtures/awf-runner.ts b/tests/fixtures/awf-runner.ts index 93dc9995..5028fee5 100644 --- a/tests/fixtures/awf-runner.ts +++ b/tests/fixtures/awf-runner.ts @@ -141,11 +141,14 @@ export class AwfRunner { // Extract work directory from stderr logs const workDir = this.extractWorkDir(result.stderr || ''); + // Normalize exit code to handle undefined (defaults to 0) + const exitCode = result.exitCode ?? 0; + return { - exitCode: result.exitCode || 0, + exitCode, stdout: result.stdout || '', stderr: result.stderr || '', - success: result.exitCode === 0, + success: exitCode === 0, timedOut: false, workDir, }; @@ -285,11 +288,14 @@ export class AwfRunner { const workDir = this.extractWorkDir(result.stderr || ''); + // Normalize exit code to handle undefined (defaults to 0) + const exitCode = result.exitCode ?? 0; + return { - exitCode: result.exitCode || 0, + exitCode, stdout: result.stdout || '', stderr: result.stderr || '', - success: result.exitCode === 0, + success: exitCode === 0, timedOut: false, workDir, };