diff --git a/.github/agentics/large-payload-tester.md b/.github/agentics/large-payload-tester.md new file mode 100644 index 0000000..e398e8b --- /dev/null +++ b/.github/agentics/large-payload-tester.md @@ -0,0 +1,142 @@ + + + +# Large MCP Payload Access Test + +You are an AI agent testing the MCP Gateway's ability to handle large payloads and make them accessible to agents. + +## Your Task + +Test that when the MCP Gateway receives large responses from backend MCP servers: +1. It correctly stores payloads to disk with proper session isolation +2. It returns metadata including the payload file path +3. Agents can successfully read the payload files from their mounted session directory + +## Test Protocol + +This test uses a **secret-based verification approach**: +1. A secret UUID is embedded in a large test file (~500KB) before the test runs +2. You will use the filesystem MCP server to read a large file containing this secret +3. The gateway will intercept the large response, store it to disk, and return metadata with a `payloadPath` +4. You must then read the payload file from the path provided and extract the secret +5. Finally, report whether you successfully retrieved the secret from the payload + +## Test Steps + +### Step 1: Read the Test Secret +- Read `/workspace/test-data/test-secret.txt` to get the secret UUID that was generated for this test run +- This file contains ONLY the secret UUID (e.g., `abc123-def456-ghi789`) +- Store this secret - you'll need it to verify payload retrieval later + +### Step 2: Trigger a Large Payload Response +- Use the filesystem MCP server's `read_file` tool to read `/workspace/test-data/large-test-file.json` +- This file is ~500KB and contains the secret embedded in JSON data +- The gateway should intercept this response and store it to disk + +### Step 3: Extract Metadata from Gateway Response +The gateway's jqschema middleware should transform the response to include: +- `payloadPath`: Full path to the stored payload file +- `preview`: First 500 characters of the response +- `schema`: JSON schema showing structure +- `originalSize`: Size of the full payload +- `queryID`: Unique identifier for this tool call +- `truncated`: Boolean indicating if preview was truncated + +Extract and log: +- The `payloadPath` value +- The `queryID` value +- Whether `truncated` is `true` +- The `originalSize` value + +### Step 4: Read the Payload File +The payload path will be in the format: `/tmp/jq-payloads/{sessionID}/{queryID}/payload.json` + +**IMPORTANT**: The agent's payload directory is mounted to the agent's container. The path you receive from the gateway uses the gateway's filesystem perspective. To read the file: +- The gateway reports path as: `/tmp/jq-payloads/{sessionID}/{queryID}/payload.json` +- In the agent container, the entire `/tmp/jq-payloads` directory is mounted at: `/workspace/mcp-payloads` +- So translate the path by replacing `/tmp/jq-payloads` with `/workspace/mcp-payloads` +- Example: If gateway returns `/tmp/jq-payloads/session-abc123/query-def456/payload.json`, use `/workspace/mcp-payloads/session-abc123/query-def456/payload.json` +- The `{sessionID}` is the actual session identifier, not the literal word "session" +- Use the filesystem MCP server to read the translated path + +Use the filesystem MCP server's `read_file` tool to read the payload file at the translated path. + +### Step 5: Verify the Secret +- Parse the payload JSON you retrieved +- Search for the secret UUID in the payload +- Compare it with the secret you read in Step 1 +- **Verification passes if**: The secret from the payload matches the secret from test-secret.txt +- **Verification fails if**: The secret is missing, doesn't match, or you couldn't read the payload file + +### Step 6: Report Results +Create a summary of the test results including: +1. ✅ or ❌ for each test step +2. The secret value you expected (from test-secret.txt) +3. The secret value you found (from the payload file) +4. Whether secrets matched (PASS/FAIL) +5. Path information (gateway path and agent path used) +6. Any errors encountered + +## Important Notes + +- **Keep all outputs concise** - Use brief, factual statements +- **Log all key values** - Secret, paths, sizes, queryID +- **Be explicit about failures** - State exactly what went wrong if any step fails +- **Path translation is critical** - The gateway and agent see different filesystem paths due to volume mounts + +## Expected Behavior + +**Success scenario:** +1. Gateway receives large response from filesystem server +2. Gateway stores payload to: `/tmp/jq-payloads/{sessionID}/{queryID}/payload.json` +3. Gateway returns metadata with `payloadPath` and `truncated: true` +4. Agent reads payload from mounted path: `/workspace/mcp-payloads/session/{queryID}/payload.json` +5. Agent extracts secret from payload +6. Secret matches the expected value from test-secret.txt + +**Failure scenarios to detect:** +- Gateway doesn't intercept/store large payloads (no payloadPath in response) +- Gateway path is incorrect or inaccessible +- Agent can't read payload file (permission/mount issues) +- Payload is corrupted or incomplete +- Secret doesn't match (data integrity issue) + +## Output Format + +After running all tests, create an issue with: +- Title: "Large Payload Test - ${{ github.run_id }}" +- Body with test results in this format: + +```markdown +# Large MCP Payload Access Test Results + +**Run ID:** ${{ github.run_id }} +**Status:** [PASS/FAIL] +**Timestamp:** [current time] + +## Test Results + +1. ✅/❌ Read test secret from control file +2. ✅/❌ Trigger large payload response (>1KB) +3. ✅/❌ Receive gateway metadata with payloadPath +4. ✅/❌ Translate and access payload file path +5. ✅/❌ Read payload file contents +6. ✅/❌ Extract and verify secret + +## Details + +- **Expected Secret:** [UUID from test-secret.txt] +- **Found Secret:** [UUID from payload] or "NOT FOUND" +- **Secret Match:** [YES/NO] +- **Gateway Path:** [path from response] +- **Agent Path:** [translated path used] +- **Payload Size:** [originalSize from metadata] +- **Query ID:** [queryID from metadata] + +## Conclusion + +[Brief summary of what worked and what failed, if anything] + +--- +Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} +``` diff --git a/.github/workflows/large-payload-tester-README.md b/.github/workflows/large-payload-tester-README.md new file mode 100644 index 0000000..71affb0 --- /dev/null +++ b/.github/workflows/large-payload-tester-README.md @@ -0,0 +1,161 @@ +# Large Payload Tester Workflow + +## Purpose + +This agentic workflow tests the MCP Gateway's large payload handling feature, specifically: +1. **Payload Storage**: Verifies that large responses (>500 chars) are automatically stored to disk +2. **Metadata Response**: Confirms the gateway returns correct metadata including `payloadPath`, `schema`, `preview`, etc. +3. **Agent Access**: Tests that agents can successfully read the payload files from their mounted directories +4. **Session Isolation**: Validates that payload files are organized by session ID for multi-agent isolation + +## How It Works + +### Test Architecture + +``` +┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐ +│ Agent │ │ MCP Gateway │ │ Filesystem │ +│ Container │◄────────►│ Container │◄────────►│ MCP Server │ +└─────────────────┘ └──────────────────┘ └────────────────┘ + │ │ │ + │ │ │ + Reads payload Stores payload Reads large file + from mounted dir to /tmp/jq-payloads from /tmp/mcp-test-fs + │ │ │ + ▼ ▼ ▼ + /workspace/ /tmp/jq-payloads/ /tmp/mcp-test-fs/ + mcp-payloads/ {sessionID}/ large-test-file.json + {queryID}/ (contains secret) + payload.json +``` + +### Test Protocol + +The workflow uses a **secret-based verification** approach: + +1. **Setup Phase** (bash step): + - Generate a unique UUID secret + - Create `/tmp/mcp-test-fs/test-secret.txt` containing just the secret + - Create `/tmp/mcp-test-fs/large-test-file.json` (~500KB) with: + - The secret embedded in JSON data + - Array of 2000 items each referencing the secret + - 400KB of padding to ensure size ~500KB + +2. **Test Phase** (agent): + - **Step 1**: Read `/workspace/test-data/test-secret.txt` to get the expected secret + - **Step 2**: Call filesystem MCP server to read `/workspace/test-data/large-test-file.json` + - **Step 3**: Gateway intercepts large response, stores to disk, returns metadata + - **Step 4**: Extract `payloadPath` and `queryID` from metadata + - **Step 5**: Translate path and read from `/workspace/mcp-payloads/{sessionID}/{queryID}/payload.json` + - **Step 6**: Extract secret from payload and verify it matches expected secret + +3. **Report Phase** (safe-output): + - Create GitHub issue with test results + - Report pass/fail for each step + - Include secret comparison results + +### Volume Mounts + +The workflow uses three volume mounts to enable the test: + +1. **Test Data Mount** (filesystem MCP server): + ```yaml + /tmp/mcp-test-fs:/workspace/test-data:ro + ``` + - Contains the control secret file and large test file + - Read-only access for safety + - Accessible to agent via `/workspace/test-data/` + +2. **Payload Mount** (filesystem MCP server): + ```yaml + /tmp/jq-payloads:/workspace/mcp-payloads:ro + ``` + - Allows agent to read stored payloads + - Read-only to prevent accidental corruption + - Accessible to agent via `/workspace/mcp-payloads/` + +3. **Gateway Payload Mount** (MCP gateway container): + ```yaml + /tmp/jq-payloads:/tmp/jq-payloads:rw + ``` + - Allows gateway to write payload files + - Read-write for payload storage + +### Path Translation + +The agent must translate paths between gateway and agent perspectives: + +- **Gateway reports**: `/tmp/jq-payloads/{sessionID}/{queryID}/payload.json` +- **Agent uses**: `/workspace/mcp-payloads/{sessionID}/{queryID}/payload.json` +- **Translation rule**: Replace `/tmp/jq-payloads` → `/workspace/mcp-payloads` + +## Expected Behavior + +### Success Scenario + +When working correctly: +1. Gateway intercepts the large file read response +2. Gateway stores payload to disk with structure: `{payloadDir}/{sessionID}/{queryID}/payload.json` +3. Gateway returns metadata with `truncated: true` and `payloadPath` +4. Agent translates path and successfully reads payload file +5. Agent extracts secret from payload +6. Secret matches the expected value +7. Test reports **PASS** + +### Failure Scenarios + +The test is designed to detect: +- **Gateway not intercepting**: No `payloadPath` in response +- **Wrong path structure**: Agent can't find the file +- **Permission issues**: Agent can't read the payload file +- **Mount problems**: Volume mounts not configured correctly +- **Data corruption**: Secret in payload doesn't match expected +- **Session isolation broken**: Wrong session directory used + +## Files + +- `.github/workflows/large-payload-tester.md` - Workflow definition with frontmatter and setup steps +- `.github/agentics/large-payload-tester.md` - Agent prompt with detailed test instructions +- `.github/workflows/large-payload-tester.lock.yml` - Compiled GitHub Actions workflow + +## Triggering + +- **Manual**: `workflow_dispatch` - Can be triggered manually from GitHub UI +- **Scheduled**: Runs daily at a scattered time (around 1:12 AM UTC) + +## Configuration + +Key configuration in frontmatter: +- `strict: true` - Enforces security best practices +- `timeout-minutes: 10` - Reasonable timeout for the test +- `network.allowed: [defaults]` - Minimal network access +- `tools.bash: ["*"]` - Full bash access for setup steps +- `mcp-servers.filesystem` - Configured with two volume mounts + +## Related Features + +This workflow tests the jqschema middleware feature. The related implementation files are: +- `internal/middleware/jqschema.go` - Middleware that intercepts large responses +- `internal/middleware/README.md` - Documentation of the jqschema middleware +- `internal/config/config_payload.go` - Payload directory configuration +- `test/integration/large_payload_test.go` - Unit/integration tests for payload handling + +These files already exist in the repository and implement the feature being tested by this workflow. + +## Security Considerations + +- **Read-only mounts**: Agent has read-only access to payload directory +- **Session isolation**: Each session gets its own subdirectory +- **Payload cleanup**: Old payloads are not automatically cleaned up (manual cleanup needed) +- **File permissions**: Payload files created with `0600` (owner read/write only) +- **Secret handling**: Test secret is only used for this test and is not sensitive + +## Future Enhancements + +Potential improvements: +1. Test with multiple concurrent sessions +2. Test with very large payloads (>10MB) +3. Test payload cleanup mechanisms +4. Add performance metrics (storage time, read time) +5. Test error handling (disk full, permission denied) +6. Verify jq schema accuracy against complex data structures diff --git a/.github/workflows/large-payload-tester.lock.yml b/.github/workflows/large-payload-tester.lock.yml new file mode 100644 index 0000000..02a3c6f --- /dev/null +++ b/.github/workflows/large-payload-tester.lock.yml @@ -0,0 +1,1017 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.42.3). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# +# Test the MCP Gateway's ability to handle large payloads and provide agent access to stored payload files +# +# frontmatter-hash: fa508aa64126acc64d01b27a60b22e5cae01819fe409b71943d2266c9b750d21 + +name: "Large Payload Tester" +"on": + schedule: + - cron: "12 1 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Large Payload Tester" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.3 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_WORKFLOW_FILE: "large-payload-tester.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.3 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + sparse-checkout: | + .github + .agents + depth: 1 + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Setup Test Environment + run: "# Create test directories\nmkdir -p /tmp/mcp-test-fs\nmkdir -p /tmp/jq-payloads\n\n# Generate a unique secret for this test run\n# Use uuidgen if available, otherwise use timestamp with nanoseconds for better entropy\nif command -v uuidgen >/dev/null 2>&1; then\n TEST_SECRET=\"test-secret-$(uuidgen)\"\nelse\n TEST_SECRET=\"test-secret-$(date +%s%N)-$$\"\nfi\necho \"$TEST_SECRET\" > /tmp/mcp-test-fs/test-secret.txt\n\n# Create a large test file (~500KB) with the secret embedded in JSON\n# This file will be read by the filesystem MCP server, causing a large payload\ncat > /tmp/mcp-test-fs/large-test-file.json <<'EOF'\n{\n \"test_run_id\": \"PLACEHOLDER_RUN_ID\",\n \"test_secret\": \"PLACEHOLDER_SECRET\",\n \"test_timestamp\": \"PLACEHOLDER_TIMESTAMP\",\n \"purpose\": \"Testing large MCP payload storage and retrieval\",\n \"data\": {\n \"large_array\": [],\n \"metadata\": {\n \"generated_by\": \"large-payload-tester workflow\",\n \"repository\": \"PLACEHOLDER_REPO\",\n \"workflow_run_url\": \"PLACEHOLDER_URL\"\n }\n },\n \"padding\": \"\"\n}\nEOF\n\n# Use jq to properly populate the JSON with dynamic values and generate large array\n# Generating 2000 items + 400KB padding to create ~500KB file\njq --arg secret \"$TEST_SECRET\" \\\n --arg run_id \"${{ github.run_id }}\" \\\n --arg timestamp \"$(date -Iseconds)\" \\\n --arg repo \"${{ github.repository }}\" \\\n --arg url \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\" \\\n '.test_secret = $secret | \n .test_run_id = $run_id | \n .test_timestamp = $timestamp | \n .data.metadata.repository = $repo | \n .data.metadata.workflow_run_url = $url | \n .data.large_array = [range(2000) | {id: ., value: (\"item-\" + tostring), secret_reference: $secret, extra_data: (\"data-\" + tostring + \"-\" * 50)}] |\n .padding = (\"X\" * 400000)' \\\n /tmp/mcp-test-fs/large-test-file.json > /tmp/mcp-test-fs/large-test-file.json.tmp\n\nmv /tmp/mcp-test-fs/large-test-file.json.tmp /tmp/mcp-test-fs/large-test-file.json\n\n# Verify file was created and is large enough\nFILE_SIZE=$(wc -c < /tmp/mcp-test-fs/large-test-file.json)\necho \"Created large-test-file.json with size: $FILE_SIZE bytes (~$(($FILE_SIZE / 1024))KB)\"\nif [ \"$FILE_SIZE\" -lt 512000 ]; then\n echo \"WARNING: Test file is smaller than expected ($FILE_SIZE bytes < 500KB)\"\n echo \"Continuing with test anyway...\"\nfi\n\n# Verify secret file was created\nif [ ! -f /tmp/mcp-test-fs/test-secret.txt ]; then\n echo \"ERROR: Secret file was not created\"\n exit 1\nfi\n\necho \"Test environment setup complete\"\necho \"Secret stored in: /tmp/mcp-test-fs/test-secret.txt\"\necho \"Large file stored in: /tmp/mcp-test-fs/large-test-file.json\"\n" + + - 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: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - 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.403 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.4 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + env: + TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + if: env.TOKEN_CHECK != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + 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-act:0.13.4 ghcr.io/github/gh-aw-firewall/squid:0.13.4 ghcr.io/github/gh-aw-mcpg:v0.0.99 ghcr.io/github/github-mcp-server:v0.30.3 mcp/filesystem 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' + {"create_issue":{"max":10},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"[large-payload-test] \". Labels [mcp-gateway test automation] will be automatically added.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + 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="" + 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}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # 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 '/+=') + export MCP_GATEWAY_API_KEY + 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 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/jq-payloads:/tmp/jq-payloads:rw ghcr.io/github/gh-aw-mcpg:v0.0.99' + + mkdir -p /home/runner/.copilot + cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "filesystem": { + "type": "stdio", + "container": "mcp/filesystem", + "mounts": [ + "/tmp/mcp-test-fs:/workspace/test-data:ro", + "/tmp/jq-payloads:/workspace/mcp-payloads:ro" + ], + "tools": [ + "*" + ], + "env": { + "ALLOWED_PATHS": "/workspace" + } + }, + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}" + } + } + 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.403", + cli_version: "v0.42.3", + workflow_name: "Large Payload Tester", + 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"], + firewall_enabled: true, + awf_version: "v0.13.4", + awmg_version: "v0.0.99", + 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: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + + 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" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/large-payload-tester.md}} + PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 10 + run: | + set -o pipefail + sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains 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,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.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.4 --skip-pull --agent-image act \ + -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ + 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + 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 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "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,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.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.3 + 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 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + 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 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Large Payload Tester" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Large Payload Tester" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Large Payload Tester" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Large Payload Tester" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.3 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKFLOW_NAME: "Large Payload Tester" + WORKFLOW_DESCRIPTION: "Test the MCP Gateway's ability to handle large payloads and provide agent access to stored payload files" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - 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.403 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "large-payload-tester" + GH_AW_WORKFLOW_NAME: "Large Payload Tester" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.3 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + 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 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"mcp-gateway\",\"test\",\"automation\"],\"max\":10,\"title_prefix\":\"[large-payload-test] \"},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + diff --git a/.github/workflows/large-payload-tester.md b/.github/workflows/large-payload-tester.md new file mode 100644 index 0000000..31e611a --- /dev/null +++ b/.github/workflows/large-payload-tester.md @@ -0,0 +1,121 @@ +--- +name: Large Payload Tester +description: Test the MCP Gateway's ability to handle large payloads and provide agent access to stored payload files +on: + workflow_dispatch: + schedule: daily + +permissions: + contents: read + issues: read + +roles: [admin, maintainer, write] + +network: + allowed: + - defaults + +tools: + bash: ["*"] + +mcp-servers: + filesystem: + type: stdio + container: "mcp/filesystem" + env: + ALLOWED_PATHS: "/workspace" + mounts: + - "/tmp/mcp-test-fs:/workspace/test-data:ro" + - "/tmp/jq-payloads:/workspace/mcp-payloads:ro" + +sandbox: + mcp: + container: "ghcr.io/github/gh-aw-mcpg" + mounts: + - "/tmp/jq-payloads:/tmp/jq-payloads:rw" + +safe-outputs: + create-issue: + title-prefix: "[large-payload-test] " + labels: [mcp-gateway, test, automation] + max: 10 + close-older-issues: true + +steps: + - name: Setup Test Environment + run: | + # Create test directories + mkdir -p /tmp/mcp-test-fs + mkdir -p /tmp/jq-payloads + + # Generate a unique secret for this test run + # Use uuidgen if available, otherwise use timestamp with nanoseconds for better entropy + if command -v uuidgen >/dev/null 2>&1; then + TEST_SECRET="test-secret-$(uuidgen)" + else + TEST_SECRET="test-secret-$(date +%s%N)-$$" + fi + echo "$TEST_SECRET" > /tmp/mcp-test-fs/test-secret.txt + + # Create a large test file (~500KB) with the secret embedded in JSON + # This file will be read by the filesystem MCP server, causing a large payload + cat > /tmp/mcp-test-fs/large-test-file.json <<'EOF' + { + "test_run_id": "PLACEHOLDER_RUN_ID", + "test_secret": "PLACEHOLDER_SECRET", + "test_timestamp": "PLACEHOLDER_TIMESTAMP", + "purpose": "Testing large MCP payload storage and retrieval", + "data": { + "large_array": [], + "metadata": { + "generated_by": "large-payload-tester workflow", + "repository": "PLACEHOLDER_REPO", + "workflow_run_url": "PLACEHOLDER_URL" + } + }, + "padding": "" + } + EOF + + # Use jq to properly populate the JSON with dynamic values and generate large array + # Generating 2000 items + 400KB padding to create ~500KB file + jq --arg secret "$TEST_SECRET" \ + --arg run_id "${{ github.run_id }}" \ + --arg timestamp "$(date -Iseconds)" \ + --arg repo "${{ github.repository }}" \ + --arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + '.test_secret = $secret | + .test_run_id = $run_id | + .test_timestamp = $timestamp | + .data.metadata.repository = $repo | + .data.metadata.workflow_run_url = $url | + .data.large_array = [range(2000) | {id: ., value: ("item-" + tostring), secret_reference: $secret, extra_data: ("data-" + tostring + "-" * 50)}] | + .padding = ("X" * 400000)' \ + /tmp/mcp-test-fs/large-test-file.json > /tmp/mcp-test-fs/large-test-file.json.tmp + + mv /tmp/mcp-test-fs/large-test-file.json.tmp /tmp/mcp-test-fs/large-test-file.json + + # Verify file was created and is large enough + FILE_SIZE=$(wc -c < /tmp/mcp-test-fs/large-test-file.json) + echo "Created large-test-file.json with size: $FILE_SIZE bytes (~$(($FILE_SIZE / 1024))KB)" + if [ "$FILE_SIZE" -lt 512000 ]; then + echo "WARNING: Test file is smaller than expected ($FILE_SIZE bytes < 500KB)" + echo "Continuing with test anyway..." + fi + + # Verify secret file was created + if [ ! -f /tmp/mcp-test-fs/test-secret.txt ]; then + echo "ERROR: Secret file was not created" + exit 1 + fi + + echo "Test environment setup complete" + echo "Secret stored in: /tmp/mcp-test-fs/test-secret.txt" + echo "Large file stored in: /tmp/mcp-test-fs/large-test-file.json" + +timeout-minutes: 10 +strict: true +--- + + +@./agentics/large-payload-tester.md