diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index 4750f51cf0..5fef698408 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -3008,17 +3008,18 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); - try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); - } catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); + const port = parseInt(process.env.GH_AW_SAFE_INPUTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_SAFE_INPUTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: false, + logDir: "/tmp/gh-aw/safe-inputs/logs" + }).catch(error => { + console.error("Failed to start safe-inputs HTTP server:", error); process.exit(1); - } + }); EOFSI chmod +x /tmp/gh-aw/safe-inputs/mcp-server.cjs @@ -3038,9 +3039,104 @@ jobs: EOFSH_gh chmod +x /tmp/gh-aw/safe-inputs/gh.sh + - name: Generate Safe Inputs MCP Server Config + id: safe-inputs-config + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + function generateSafeInputsConfig({ core, crypto }) { + const apiKeyBuffer = crypto.randomBytes(45); + const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + const port = 3000; + core.setOutput("safe_inputs_api_key", apiKey); + core.setOutput("safe_inputs_port", port.toString()); + core.info(`Safe Inputs MCP server will run on port ${port}`); + return { apiKey, port }; + } + + // Execute the function + const crypto = require('crypto'); + generateSafeInputsConfig({ core, crypto }); + + - name: Start Safe Inputs MCP HTTP Server + id: safe-inputs-start + run: | + # Set environment variables for the server + export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }} + export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} + + export GH_AW_GH_TOKEN="${GH_AW_GH_TOKEN}" + export GH_DEBUG="${GH_DEBUG}" + + cd /tmp/gh-aw/safe-inputs + # Verify required files exist + echo "Verifying safe-inputs setup..." + if [ ! -f mcp-server.cjs ]; then + echo "ERROR: mcp-server.cjs not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + if [ ! -f tools.json ]; then + echo "ERROR: tools.json not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + echo "Configuration files verified" + # Log environment configuration + echo "Server configuration:" + echo " Port: $GH_AW_SAFE_INPUTS_PORT" + echo " API Key: ${GH_AW_SAFE_INPUTS_API_KEY:0:8}..." + echo " Working directory: $(pwd)" + # Ensure logs directory exists + mkdir -p /tmp/gh-aw/safe-inputs/logs + # Create initial server.log file for artifact upload + { + echo "Safe Inputs MCP Server Log" + echo "Start time: $(date)" + echo "===========================================" + echo "" + } > /tmp/gh-aw/safe-inputs/logs/server.log + # Start the HTTP server in the background + echo "Starting safe-inputs MCP HTTP server..." + node mcp-server.cjs >> /tmp/gh-aw/safe-inputs/logs/server.log 2>&1 & + SERVER_PID=$! + echo "Started safe-inputs MCP server with PID $SERVER_PID" + # Wait for server to be ready (max 10 seconds) + echo "Waiting for server to become ready..." + for i in {1..10}; do + # Check if process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID has died" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + exit 1 + fi + # Check if server is responding + if curl -s -f "http://localhost:$GH_AW_SAFE_INPUTS_PORT/health" > /dev/null 2>&1; then + echo "Safe Inputs MCP server is ready (attempt $i/10)" + break + fi + if [ "$i" -eq 10 ]; then + echo "ERROR: Safe Inputs MCP server failed to start after 10 seconds" + echo "Process status: $(pgrep -f 'mcp-server.cjs' || echo 'not running')" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + echo "Checking port availability:" + netstat -tuln | grep "$GH_AW_SAFE_INPUTS_PORT" || echo "Port $GH_AW_SAFE_INPUTS_PORT not listening" + exit 1 + fi + echo "Waiting for server... (attempt $i/10)" + sleep 1 + done + # Output the configuration for the MCP client + echo "port=$GH_AW_SAFE_INPUTS_PORT" >> "$GITHUB_OUTPUT" + echo "api_key=$GH_AW_SAFE_INPUTS_API_KEY" >> "$GITHUB_OUTPUT" + - name: Setup MCPs env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} + GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 run: | @@ -3050,11 +3146,15 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "local", - "command": "node", - "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], + "type": "http", + "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", + "headers": { + "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" + }, "tools": ["*"], "env": { + "GH_AW_SAFE_INPUTS_PORT": "\${GH_AW_SAFE_INPUTS_PORT}", + "GH_AW_SAFE_INPUTS_API_KEY": "\${GH_AW_SAFE_INPUTS_API_KEY}", "GH_AW_GH_TOKEN": "\${GH_AW_GH_TOKEN}", "GH_DEBUG": "\${GH_DEBUG}" } diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 0e6ddb0bd7..379aeb5de8 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -3018,17 +3018,18 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); - try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); - } catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); + const port = parseInt(process.env.GH_AW_SAFE_INPUTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_SAFE_INPUTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: false, + logDir: "/tmp/gh-aw/safe-inputs/logs" + }).catch(error => { + console.error("Failed to start safe-inputs HTTP server:", error); process.exit(1); - } + }); EOFSI chmod +x /tmp/gh-aw/safe-inputs/mcp-server.cjs @@ -3048,9 +3049,104 @@ jobs: EOFSH_gh chmod +x /tmp/gh-aw/safe-inputs/gh.sh + - name: Generate Safe Inputs MCP Server Config + id: safe-inputs-config + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + function generateSafeInputsConfig({ core, crypto }) { + const apiKeyBuffer = crypto.randomBytes(45); + const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + const port = 3000; + core.setOutput("safe_inputs_api_key", apiKey); + core.setOutput("safe_inputs_port", port.toString()); + core.info(`Safe Inputs MCP server will run on port ${port}`); + return { apiKey, port }; + } + + // Execute the function + const crypto = require('crypto'); + generateSafeInputsConfig({ core, crypto }); + + - name: Start Safe Inputs MCP HTTP Server + id: safe-inputs-start + run: | + # Set environment variables for the server + export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }} + export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} + + export GH_AW_GH_TOKEN="${GH_AW_GH_TOKEN}" + export GH_DEBUG="${GH_DEBUG}" + + cd /tmp/gh-aw/safe-inputs + # Verify required files exist + echo "Verifying safe-inputs setup..." + if [ ! -f mcp-server.cjs ]; then + echo "ERROR: mcp-server.cjs not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + if [ ! -f tools.json ]; then + echo "ERROR: tools.json not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + echo "Configuration files verified" + # Log environment configuration + echo "Server configuration:" + echo " Port: $GH_AW_SAFE_INPUTS_PORT" + echo " API Key: ${GH_AW_SAFE_INPUTS_API_KEY:0:8}..." + echo " Working directory: $(pwd)" + # Ensure logs directory exists + mkdir -p /tmp/gh-aw/safe-inputs/logs + # Create initial server.log file for artifact upload + { + echo "Safe Inputs MCP Server Log" + echo "Start time: $(date)" + echo "===========================================" + echo "" + } > /tmp/gh-aw/safe-inputs/logs/server.log + # Start the HTTP server in the background + echo "Starting safe-inputs MCP HTTP server..." + node mcp-server.cjs >> /tmp/gh-aw/safe-inputs/logs/server.log 2>&1 & + SERVER_PID=$! + echo "Started safe-inputs MCP server with PID $SERVER_PID" + # Wait for server to be ready (max 10 seconds) + echo "Waiting for server to become ready..." + for i in {1..10}; do + # Check if process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID has died" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + exit 1 + fi + # Check if server is responding + if curl -s -f "http://localhost:$GH_AW_SAFE_INPUTS_PORT/health" > /dev/null 2>&1; then + echo "Safe Inputs MCP server is ready (attempt $i/10)" + break + fi + if [ "$i" -eq 10 ]; then + echo "ERROR: Safe Inputs MCP server failed to start after 10 seconds" + echo "Process status: $(pgrep -f 'mcp-server.cjs' || echo 'not running')" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + echo "Checking port availability:" + netstat -tuln | grep "$GH_AW_SAFE_INPUTS_PORT" || echo "Port $GH_AW_SAFE_INPUTS_PORT not listening" + exit 1 + fi + echo "Waiting for server... (attempt $i/10)" + sleep 1 + done + # Output the configuration for the MCP client + echo "port=$GH_AW_SAFE_INPUTS_PORT" >> "$GITHUB_OUTPUT" + echo "api_key=$GH_AW_SAFE_INPUTS_API_KEY" >> "$GITHUB_OUTPUT" + - name: Setup MCPs env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} + GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 run: | @@ -3060,11 +3156,15 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "local", - "command": "node", - "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], + "type": "http", + "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", + "headers": { + "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" + }, "tools": ["*"], "env": { + "GH_AW_SAFE_INPUTS_PORT": "\${GH_AW_SAFE_INPUTS_PORT}", + "GH_AW_SAFE_INPUTS_API_KEY": "\${GH_AW_SAFE_INPUTS_API_KEY}", "GH_AW_GH_TOKEN": "\${GH_AW_GH_TOKEN}", "GH_DEBUG": "\${GH_DEBUG}" } diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index db78e27369..c2cdfcbecb 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -3477,17 +3477,18 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); - try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); - } catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); + const port = parseInt(process.env.GH_AW_SAFE_INPUTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_SAFE_INPUTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: false, + logDir: "/tmp/gh-aw/safe-inputs/logs" + }).catch(error => { + console.error("Failed to start safe-inputs HTTP server:", error); process.exit(1); - } + }); EOFSI chmod +x /tmp/gh-aw/safe-inputs/mcp-server.cjs @@ -3507,10 +3508,105 @@ jobs: EOFSH_gh chmod +x /tmp/gh-aw/safe-inputs/gh.sh + - name: Generate Safe Inputs MCP Server Config + id: safe-inputs-config + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + function generateSafeInputsConfig({ core, crypto }) { + const apiKeyBuffer = crypto.randomBytes(45); + const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + const port = 3000; + core.setOutput("safe_inputs_api_key", apiKey); + core.setOutput("safe_inputs_port", port.toString()); + core.info(`Safe Inputs MCP server will run on port ${port}`); + return { apiKey, port }; + } + + // Execute the function + const crypto = require('crypto'); + generateSafeInputsConfig({ core, crypto }); + + - name: Start Safe Inputs MCP HTTP Server + id: safe-inputs-start + run: | + # Set environment variables for the server + export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }} + export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} + + export GH_AW_GH_TOKEN="${GH_AW_GH_TOKEN}" + export GH_DEBUG="${GH_DEBUG}" + + cd /tmp/gh-aw/safe-inputs + # Verify required files exist + echo "Verifying safe-inputs setup..." + if [ ! -f mcp-server.cjs ]; then + echo "ERROR: mcp-server.cjs not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + if [ ! -f tools.json ]; then + echo "ERROR: tools.json not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + echo "Configuration files verified" + # Log environment configuration + echo "Server configuration:" + echo " Port: $GH_AW_SAFE_INPUTS_PORT" + echo " API Key: ${GH_AW_SAFE_INPUTS_API_KEY:0:8}..." + echo " Working directory: $(pwd)" + # Ensure logs directory exists + mkdir -p /tmp/gh-aw/safe-inputs/logs + # Create initial server.log file for artifact upload + { + echo "Safe Inputs MCP Server Log" + echo "Start time: $(date)" + echo "===========================================" + echo "" + } > /tmp/gh-aw/safe-inputs/logs/server.log + # Start the HTTP server in the background + echo "Starting safe-inputs MCP HTTP server..." + node mcp-server.cjs >> /tmp/gh-aw/safe-inputs/logs/server.log 2>&1 & + SERVER_PID=$! + echo "Started safe-inputs MCP server with PID $SERVER_PID" + # Wait for server to be ready (max 10 seconds) + echo "Waiting for server to become ready..." + for i in {1..10}; do + # Check if process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID has died" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + exit 1 + fi + # Check if server is responding + if curl -s -f "http://localhost:$GH_AW_SAFE_INPUTS_PORT/health" > /dev/null 2>&1; then + echo "Safe Inputs MCP server is ready (attempt $i/10)" + break + fi + if [ "$i" -eq 10 ]; then + echo "ERROR: Safe Inputs MCP server failed to start after 10 seconds" + echo "Process status: $(pgrep -f 'mcp-server.cjs' || echo 'not running')" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + echo "Checking port availability:" + netstat -tuln | grep "$GH_AW_SAFE_INPUTS_PORT" || echo "Port $GH_AW_SAFE_INPUTS_PORT not listening" + exit 1 + fi + echo "Waiting for server... (attempt $i/10)" + sleep 1 + done + # Output the configuration for the MCP client + echo "port=$GH_AW_SAFE_INPUTS_PORT" >> "$GITHUB_OUTPUT" + echo "api_key=$GH_AW_SAFE_INPUTS_API_KEY" >> "$GITHUB_OUTPUT" + - name: Setup MCPs env: GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} + GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 run: | @@ -3546,11 +3642,15 @@ jobs: "tools": ["*"] }, "safeinputs": { - "type": "local", - "command": "node", - "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], + "type": "http", + "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", + "headers": { + "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" + }, "tools": ["*"], "env": { + "GH_AW_SAFE_INPUTS_PORT": "\${GH_AW_SAFE_INPUTS_PORT}", + "GH_AW_SAFE_INPUTS_API_KEY": "\${GH_AW_SAFE_INPUTS_API_KEY}", "GH_AW_GH_TOKEN": "\${GH_AW_GH_TOKEN}", "GH_DEBUG": "\${GH_DEBUG}" } diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml index 36ba1db97a..6d759164b7 100644 --- a/.github/workflows/smoke-copilot-playwright.lock.yml +++ b/.github/workflows/smoke-copilot-playwright.lock.yml @@ -3568,17 +3568,18 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); - try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); - } catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); + const port = parseInt(process.env.GH_AW_SAFE_INPUTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_SAFE_INPUTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: false, + logDir: "/tmp/gh-aw/safe-inputs/logs" + }).catch(error => { + console.error("Failed to start safe-inputs HTTP server:", error); process.exit(1); - } + }); EOFSI chmod +x /tmp/gh-aw/safe-inputs/mcp-server.cjs @@ -3598,10 +3599,105 @@ jobs: EOFSH_gh chmod +x /tmp/gh-aw/safe-inputs/gh.sh + - name: Generate Safe Inputs MCP Server Config + id: safe-inputs-config + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + function generateSafeInputsConfig({ core, crypto }) { + const apiKeyBuffer = crypto.randomBytes(45); + const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + const port = 3000; + core.setOutput("safe_inputs_api_key", apiKey); + core.setOutput("safe_inputs_port", port.toString()); + core.info(`Safe Inputs MCP server will run on port ${port}`); + return { apiKey, port }; + } + + // Execute the function + const crypto = require('crypto'); + generateSafeInputsConfig({ core, crypto }); + + - name: Start Safe Inputs MCP HTTP Server + id: safe-inputs-start + run: | + # Set environment variables for the server + export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }} + export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} + + export GH_AW_GH_TOKEN="${GH_AW_GH_TOKEN}" + export GH_DEBUG="${GH_DEBUG}" + + cd /tmp/gh-aw/safe-inputs + # Verify required files exist + echo "Verifying safe-inputs setup..." + if [ ! -f mcp-server.cjs ]; then + echo "ERROR: mcp-server.cjs not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + if [ ! -f tools.json ]; then + echo "ERROR: tools.json not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + echo "Configuration files verified" + # Log environment configuration + echo "Server configuration:" + echo " Port: $GH_AW_SAFE_INPUTS_PORT" + echo " API Key: ${GH_AW_SAFE_INPUTS_API_KEY:0:8}..." + echo " Working directory: $(pwd)" + # Ensure logs directory exists + mkdir -p /tmp/gh-aw/safe-inputs/logs + # Create initial server.log file for artifact upload + { + echo "Safe Inputs MCP Server Log" + echo "Start time: $(date)" + echo "===========================================" + echo "" + } > /tmp/gh-aw/safe-inputs/logs/server.log + # Start the HTTP server in the background + echo "Starting safe-inputs MCP HTTP server..." + node mcp-server.cjs >> /tmp/gh-aw/safe-inputs/logs/server.log 2>&1 & + SERVER_PID=$! + echo "Started safe-inputs MCP server with PID $SERVER_PID" + # Wait for server to be ready (max 10 seconds) + echo "Waiting for server to become ready..." + for i in {1..10}; do + # Check if process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID has died" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + exit 1 + fi + # Check if server is responding + if curl -s -f "http://localhost:$GH_AW_SAFE_INPUTS_PORT/health" > /dev/null 2>&1; then + echo "Safe Inputs MCP server is ready (attempt $i/10)" + break + fi + if [ "$i" -eq 10 ]; then + echo "ERROR: Safe Inputs MCP server failed to start after 10 seconds" + echo "Process status: $(pgrep -f 'mcp-server.cjs' || echo 'not running')" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + echo "Checking port availability:" + netstat -tuln | grep "$GH_AW_SAFE_INPUTS_PORT" || echo "Port $GH_AW_SAFE_INPUTS_PORT not listening" + exit 1 + fi + echo "Waiting for server... (attempt $i/10)" + sleep 1 + done + # Output the configuration for the MCP client + echo "port=$GH_AW_SAFE_INPUTS_PORT" >> "$GITHUB_OUTPUT" + echo "api_key=$GH_AW_SAFE_INPUTS_API_KEY" >> "$GITHUB_OUTPUT" + - name: Setup MCPs env: GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} + GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 run: | @@ -3637,11 +3733,15 @@ jobs: "tools": ["*"] }, "safeinputs": { - "type": "local", - "command": "node", - "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], + "type": "http", + "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", + "headers": { + "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" + }, "tools": ["*"], "env": { + "GH_AW_SAFE_INPUTS_PORT": "\${GH_AW_SAFE_INPUTS_PORT}", + "GH_AW_SAFE_INPUTS_API_KEY": "\${GH_AW_SAFE_INPUTS_API_KEY}", "GH_AW_GH_TOKEN": "\${GH_AW_GH_TOKEN}", "GH_DEBUG": "\${GH_DEBUG}" } diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml index f12b29db9a..ba24195c85 100644 --- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml +++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml @@ -3453,17 +3453,18 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); - try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); - } catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); + const port = parseInt(process.env.GH_AW_SAFE_INPUTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_SAFE_INPUTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: false, + logDir: "/tmp/gh-aw/safe-inputs/logs" + }).catch(error => { + console.error("Failed to start safe-inputs HTTP server:", error); process.exit(1); - } + }); EOFSI chmod +x /tmp/gh-aw/safe-inputs/mcp-server.cjs @@ -3483,9 +3484,104 @@ jobs: EOFSH_gh chmod +x /tmp/gh-aw/safe-inputs/gh.sh + - name: Generate Safe Inputs MCP Server Config + id: safe-inputs-config + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + function generateSafeInputsConfig({ core, crypto }) { + const apiKeyBuffer = crypto.randomBytes(45); + const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + const port = 3000; + core.setOutput("safe_inputs_api_key", apiKey); + core.setOutput("safe_inputs_port", port.toString()); + core.info(`Safe Inputs MCP server will run on port ${port}`); + return { apiKey, port }; + } + + // Execute the function + const crypto = require('crypto'); + generateSafeInputsConfig({ core, crypto }); + + - name: Start Safe Inputs MCP HTTP Server + id: safe-inputs-start + run: | + # Set environment variables for the server + export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }} + export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} + + export GH_AW_GH_TOKEN="${GH_AW_GH_TOKEN}" + export GH_DEBUG="${GH_DEBUG}" + + cd /tmp/gh-aw/safe-inputs + # Verify required files exist + echo "Verifying safe-inputs setup..." + if [ ! -f mcp-server.cjs ]; then + echo "ERROR: mcp-server.cjs not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + if [ ! -f tools.json ]; then + echo "ERROR: tools.json not found in /tmp/gh-aw/safe-inputs" + ls -la /tmp/gh-aw/safe-inputs/ + exit 1 + fi + echo "Configuration files verified" + # Log environment configuration + echo "Server configuration:" + echo " Port: $GH_AW_SAFE_INPUTS_PORT" + echo " API Key: ${GH_AW_SAFE_INPUTS_API_KEY:0:8}..." + echo " Working directory: $(pwd)" + # Ensure logs directory exists + mkdir -p /tmp/gh-aw/safe-inputs/logs + # Create initial server.log file for artifact upload + { + echo "Safe Inputs MCP Server Log" + echo "Start time: $(date)" + echo "===========================================" + echo "" + } > /tmp/gh-aw/safe-inputs/logs/server.log + # Start the HTTP server in the background + echo "Starting safe-inputs MCP HTTP server..." + node mcp-server.cjs >> /tmp/gh-aw/safe-inputs/logs/server.log 2>&1 & + SERVER_PID=$! + echo "Started safe-inputs MCP server with PID $SERVER_PID" + # Wait for server to be ready (max 10 seconds) + echo "Waiting for server to become ready..." + for i in {1..10}; do + # Check if process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID has died" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + exit 1 + fi + # Check if server is responding + if curl -s -f "http://localhost:$GH_AW_SAFE_INPUTS_PORT/health" > /dev/null 2>&1; then + echo "Safe Inputs MCP server is ready (attempt $i/10)" + break + fi + if [ "$i" -eq 10 ]; then + echo "ERROR: Safe Inputs MCP server failed to start after 10 seconds" + echo "Process status: $(pgrep -f 'mcp-server.cjs' || echo 'not running')" + echo "Server log contents:" + cat /tmp/gh-aw/safe-inputs/logs/server.log + echo "Checking port availability:" + netstat -tuln | grep "$GH_AW_SAFE_INPUTS_PORT" || echo "Port $GH_AW_SAFE_INPUTS_PORT not listening" + exit 1 + fi + echo "Waiting for server... (attempt $i/10)" + sleep 1 + done + # Output the configuration for the MCP client + echo "port=$GH_AW_SAFE_INPUTS_PORT" >> "$GITHUB_OUTPUT" + echo "api_key=$GH_AW_SAFE_INPUTS_API_KEY" >> "$GITHUB_OUTPUT" + - name: Setup MCPs env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} + GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 run: | @@ -3495,11 +3591,15 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "local", - "command": "node", - "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], + "type": "http", + "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", + "headers": { + "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" + }, "tools": ["*"], "env": { + "GH_AW_SAFE_INPUTS_PORT": "\${GH_AW_SAFE_INPUTS_PORT}", + "GH_AW_SAFE_INPUTS_API_KEY": "\${GH_AW_SAFE_INPUTS_API_KEY}", "GH_AW_GH_TOKEN": "\${GH_AW_GH_TOKEN}", "GH_DEBUG": "\${GH_DEBUG}" } diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index c5b822b323..b5aade72c4 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -2863,9 +2863,8 @@ strict: true # (JavaScript), 'run' (shell), or 'py' (Python) must be specified per tool. # (optional) safe-inputs: - # Transport mode for the safe-inputs MCP server. 'http' starts the server as a - # separate step (default), 'stdio' starts the server directly by the agent within - # the firewall. + # Transport mode for the safe-inputs MCP server. Only 'http' mode is supported, + # which starts the server as a separate step. # (optional) mode: "http" diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go index 2e2126f78f..eb334adc17 100644 --- a/pkg/cli/fix_codemods.go +++ b/pkg/cli/fix_codemods.go @@ -44,6 +44,7 @@ func GetAllCodemods() []Codemod { getTimeoutMinutesCodemod(), getNetworkFirewallCodemod(), getCommandToSlashCommandCodemod(), + getSafeInputsModeCodemod(), } } @@ -399,3 +400,91 @@ func getCommandToSlashCommandCodemod() Codemod { }, } } + +// getSafeInputsModeCodemod creates a codemod for removing the deprecated safe-inputs.mode field +func getSafeInputsModeCodemod() Codemod { + return Codemod{ + ID: "safe-inputs-mode-removal", + Name: "Remove deprecated safe-inputs.mode field", + Description: "Removes the deprecated 'safe-inputs.mode' field (HTTP is now the only supported mode)", + IntroducedIn: "0.2.0", + Apply: func(content string, frontmatter map[string]any) (string, bool, error) { + // Check if safe-inputs.mode exists + safeInputsValue, hasSafeInputs := frontmatter["safe-inputs"] + if !hasSafeInputs { + return content, false, nil + } + + safeInputsMap, ok := safeInputsValue.(map[string]any) + if !ok { + return content, false, nil + } + + // Check if mode field exists in safe-inputs + _, hasMode := safeInputsMap["mode"] + if !hasMode { + return content, false, nil + } + + // Parse frontmatter to get raw lines + result, err := parser.ExtractFrontmatterFromContent(content) + if err != nil { + return content, false, fmt.Errorf("failed to parse frontmatter: %w", err) + } + + // Find and remove the mode line within the safe-inputs block + var modified bool + var inSafeInputsBlock bool + var safeInputsIndent string + + frontmatterLines := make([]string, 0, len(result.FrontmatterLines)) + + for i, line := range result.FrontmatterLines { + trimmedLine := strings.TrimSpace(line) + + // Track if we're in the safe-inputs block + if strings.HasPrefix(trimmedLine, "safe-inputs:") { + inSafeInputsBlock = true + safeInputsIndent = line[:len(line)-len(strings.TrimLeft(line, " \t"))] + frontmatterLines = append(frontmatterLines, line) + continue + } + + // Check if we've left the safe-inputs block (new top-level key with same or less indentation) + if inSafeInputsBlock && len(trimmedLine) > 0 && !strings.HasPrefix(trimmedLine, "#") { + currentIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))] + if len(currentIndent) <= len(safeInputsIndent) && strings.Contains(line, ":") { + inSafeInputsBlock = false + } + } + + // Remove mode line if in safe-inputs block + if inSafeInputsBlock && strings.HasPrefix(trimmedLine, "mode:") { + modified = true + codemodsLog.Printf("Removed safe-inputs.mode on line %d", i+1) + continue + } + + frontmatterLines = append(frontmatterLines, line) + } + + if !modified { + return content, false, nil + } + + // Reconstruct the content + var lines []string + lines = append(lines, "---") + lines = append(lines, frontmatterLines...) + lines = append(lines, "---") + if result.Markdown != "" { + lines = append(lines, "") + lines = append(lines, result.Markdown) + } + + newContent := strings.Join(lines, "\n") + codemodsLog.Print("Applied safe-inputs.mode removal") + return newContent, true, nil + }, + } +} diff --git a/pkg/cli/fix_command.go b/pkg/cli/fix_command.go index 12ec50c366..ebb8e4b727 100644 --- a/pkg/cli/fix_command.go +++ b/pkg/cli/fix_command.go @@ -40,6 +40,7 @@ and migrate to new syntax. Codemods preserve formatting and comments as much as Available codemods: • timeout-minutes-migration: Replaces 'timeout_minutes' with 'timeout-minutes' • network-firewall-migration: Replaces 'network.firewall' with 'sandbox.agent: false' + • safe-inputs-mode-removal: Removes deprecated 'safe-inputs.mode' field If no workflows are specified, all Markdown files in .github/workflows will be processed. diff --git a/pkg/cli/fix_command_test.go b/pkg/cli/fix_command_test.go index dd20c0f5c7..7e75b112b0 100644 --- a/pkg/cli/fix_command_test.go +++ b/pkg/cli/fix_command_test.go @@ -443,6 +443,7 @@ func TestGetAllCodemods(t *testing.T) { "timeout-minutes-migration", "network-firewall-migration", "command-to-slash-command-migration", + "safe-inputs-mode-removal", } foundIDs := make(map[string]bool) @@ -539,3 +540,74 @@ This is a test workflow with slash command. t.Errorf("Expected on.slash_command: my-bot in updated content, got:\n%s", updatedStr) } } + +func TestFixCommand_SafeInputsModeRemoval(t *testing.T) { +// Create a temporary directory for test files +tmpDir := t.TempDir() +workflowFile := filepath.Join(tmpDir, "test-workflow.md") + +// Create a workflow with deprecated safe-inputs.mode field +content := `--- +on: workflow_dispatch +engine: copilot +safe-inputs: + mode: http + test-tool: + description: Test tool + script: | + return { result: "test" }; +--- + +# Test Workflow + +This is a test workflow with safe-inputs mode field. +` + +if err := os.WriteFile(workflowFile, []byte(content), 0644); err != nil { +t.Fatalf("Failed to create test file: %v", err) +} + +// Get the safe-inputs mode removal codemod +modeCodemod := getCodemodByID("safe-inputs-mode-removal") +if modeCodemod == nil { +t.Fatal("safe-inputs-mode-removal codemod not found") +} + +// Process the file +fixed, err := processWorkflowFile(workflowFile, []Codemod{*modeCodemod}, true, false) +if err != nil { +t.Fatalf("Failed to process workflow file: %v", err) +} + +if !fixed { +t.Error("Expected file to be fixed, but no changes were made") +} + +// Read the updated content +updatedContent, err := os.ReadFile(workflowFile) +if err != nil { +t.Fatalf("Failed to read updated file: %v", err) +} + +updatedStr := string(updatedContent) + +t.Logf("Updated content:\n%s", updatedStr) + +// Verify the change - mode field should be removed +if strings.Contains(updatedStr, "mode:") { +t.Errorf("Expected mode field to be removed, but it still exists:\n%s", updatedStr) +} + +// Verify safe-inputs block and test-tool are preserved +if !strings.Contains(updatedStr, "safe-inputs:") { +t.Error("Expected safe-inputs block to be preserved") +} + +if !strings.Contains(updatedStr, "test-tool:") { +t.Error("Expected test-tool to be preserved") +} + +if !strings.Contains(updatedStr, "description: Test tool") { +t.Error("Expected test-tool description to be preserved") +} +} diff --git a/pkg/parser/schemas/included_file_schema.json b/pkg/parser/schemas/included_file_schema.json index 8c77641ce5..ca317a40c3 100644 --- a/pkg/parser/schemas/included_file_schema.json +++ b/pkg/parser/schemas/included_file_schema.json @@ -592,9 +592,11 @@ "properties": { "mode": { "type": "string", - "enum": ["http", "stdio"], + "enum": ["http"], "default": "http", - "description": "Transport mode for the safe-inputs MCP server. 'http' starts the server as a separate step (default), 'stdio' starts the server directly by the agent within the firewall." + "description": "Deprecated: Transport mode for the safe-inputs MCP server. This field is ignored as only 'http' mode is supported. The server always starts as a separate step.", + "deprecated": true, + "x-deprecation-message": "The mode field is no longer used. Safe-inputs always uses HTTP transport." } } }, diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ec1e21f25b..31612322f5 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4828,9 +4828,11 @@ "properties": { "mode": { "type": "string", - "enum": ["http", "stdio"], + "enum": ["http"], "default": "http", - "description": "Transport mode for the safe-inputs MCP server. 'http' starts the server as a separate step (default), 'stdio' starts the server directly by the agent within the firewall." + "description": "Deprecated: Transport mode for the safe-inputs MCP server. This field is ignored as only 'http' mode is supported. The server always starts as a separate step.", + "deprecated": true, + "x-deprecation-message": "The mode field is no longer used. Safe-inputs always uses HTTP transport." } } }, diff --git a/pkg/workflow/mcp_renderer.go b/pkg/workflow/mcp_renderer.go index dd49128cf0..e5e8e50f1c 100644 --- a/pkg/workflow/mcp_renderer.go +++ b/pkg/workflow/mcp_renderer.go @@ -222,7 +222,7 @@ func (r *MCPConfigRendererUnified) RenderSafeInputsMCP(yaml *strings.Builder, sa } // renderSafeInputsTOML generates Safe Inputs MCP configuration in TOML format -// Uses HTTP transport for consistency with JSON format (Copilot/Claude) +// Uses HTTP transport exclusively func (r *MCPConfigRendererUnified) renderSafeInputsTOML(yaml *strings.Builder, safeInputs *SafeInputsConfig) { envVars := getSafeInputsEnvVars(safeInputs) diff --git a/pkg/workflow/mcp_servers.go b/pkg/workflow/mcp_servers.go index 4ba1385e97..d2fb3805fa 100644 --- a/pkg/workflow/mcp_servers.go +++ b/pkg/workflow/mcp_servers.go @@ -503,46 +503,43 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, } yaml.WriteString(" \n") - // Steps 3-4: Generate API key and start HTTP server (only for HTTP mode) - if IsSafeInputsHTTPMode(workflowData.SafeInputs) { - // Step 3: Generate API key and choose port for HTTP server using JavaScript - yaml.WriteString(" - name: Generate Safe Inputs MCP Server Config\n") - yaml.WriteString(" id: safe-inputs-config\n") - yaml.WriteString(" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1\n") - yaml.WriteString(" with:\n") - yaml.WriteString(" script: |\n") - - // Get the bundled script - configScript := getGenerateSafeInputsConfigScript() - for _, line := range FormatJavaScriptForYAML(configScript) { - yaml.WriteString(line) - } - yaml.WriteString(" \n") - yaml.WriteString(" // Execute the function\n") - yaml.WriteString(" const crypto = require('crypto');\n") - yaml.WriteString(" generateSafeInputsConfig({ core, crypto });\n") - yaml.WriteString(" \n") - - // Step 4: Start the HTTP server in the background - yaml.WriteString(" - name: Start Safe Inputs MCP HTTP Server\n") - yaml.WriteString(" id: safe-inputs-start\n") - yaml.WriteString(" run: |\n") - yaml.WriteString(" # Set environment variables for the server\n") - yaml.WriteString(" export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }}\n") - yaml.WriteString(" export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }}\n") - yaml.WriteString(" \n") - - // Pass through environment variables from safe-inputs config - envVars := getSafeInputsEnvVars(workflowData.SafeInputs) - for _, envVar := range envVars { - yaml.WriteString(fmt.Sprintf(" export %s=\"${%s}\"\n", envVar, envVar)) - } - yaml.WriteString(" \n") + // Step 3: Generate API key and choose port for HTTP server using JavaScript + yaml.WriteString(" - name: Generate Safe Inputs MCP Server Config\n") + yaml.WriteString(" id: safe-inputs-config\n") + yaml.WriteString(" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1\n") + yaml.WriteString(" with:\n") + yaml.WriteString(" script: |\n") + + // Get the bundled script + configScript := getGenerateSafeInputsConfigScript() + for _, line := range FormatJavaScriptForYAML(configScript) { + yaml.WriteString(line) + } + yaml.WriteString(" \n") + yaml.WriteString(" // Execute the function\n") + yaml.WriteString(" const crypto = require('crypto');\n") + yaml.WriteString(" generateSafeInputsConfig({ core, crypto });\n") + yaml.WriteString(" \n") - // Use the embedded shell script to start the server - WriteShellScriptToYAML(yaml, startSafeInputsServerScript, " ") - yaml.WriteString(" \n") + // Step 4: Start the HTTP server in the background + yaml.WriteString(" - name: Start Safe Inputs MCP HTTP Server\n") + yaml.WriteString(" id: safe-inputs-start\n") + yaml.WriteString(" run: |\n") + yaml.WriteString(" # Set environment variables for the server\n") + yaml.WriteString(" export GH_AW_SAFE_INPUTS_PORT=${{ steps.safe-inputs-config.outputs.safe_inputs_port }}\n") + yaml.WriteString(" export GH_AW_SAFE_INPUTS_API_KEY=${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }}\n") + yaml.WriteString(" \n") + + // Pass through environment variables from safe-inputs config + envVars := getSafeInputsEnvVars(workflowData.SafeInputs) + for _, envVar := range envVars { + yaml.WriteString(fmt.Sprintf(" export %s=\"${%s}\"\n", envVar, envVar)) } + yaml.WriteString(" \n") + + // Use the embedded shell script to start the server + WriteShellScriptToYAML(yaml, startSafeInputsServerScript, " ") + yaml.WriteString(" \n") } // Use the engine's RenderMCPConfig method @@ -612,13 +609,11 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, // Add safe-inputs env vars if present if hasSafeInputs { - // Add server configuration env vars from step outputs (HTTP mode only) - if IsSafeInputsHTTPMode(workflowData.SafeInputs) { - yaml.WriteString(" GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }}\n") - yaml.WriteString(" GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }}\n") - } + // Add server configuration env vars from step outputs + yaml.WriteString(" GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }}\n") + yaml.WriteString(" GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }}\n") - // Add tool-specific env vars (secrets passthrough) - needed for both modes + // Add tool-specific env vars (secrets passthrough) safeInputsSecrets := collectSafeInputsSecrets(workflowData.SafeInputs) if len(safeInputsSecrets) > 0 { // Sort env var names for consistent output diff --git a/pkg/workflow/safe_inputs.go b/pkg/workflow/safe_inputs.go index 00231f37d6..e5c1061224 100644 --- a/pkg/workflow/safe_inputs.go +++ b/pkg/workflow/safe_inputs.go @@ -59,8 +59,7 @@ type SafeInputParam struct { // SafeInputsMode constants define the available transport modes const ( - SafeInputsModeHTTP = "http" - SafeInputsModeStdio = "stdio" + SafeInputsModeHTTP = "http" ) // HasSafeInputs checks if safe-inputs are configured @@ -68,14 +67,10 @@ func HasSafeInputs(safeInputs *SafeInputsConfig) bool { return safeInputs != nil && len(safeInputs.Tools) > 0 } -// IsSafeInputsStdioMode checks if safe-inputs is configured to use stdio mode -func IsSafeInputsStdioMode(safeInputs *SafeInputsConfig) bool { - return safeInputs != nil && safeInputs.Mode == SafeInputsModeStdio -} - // IsSafeInputsHTTPMode checks if safe-inputs is configured to use HTTP mode +// Note: All safe-inputs configurations now use HTTP mode exclusively func IsSafeInputsHTTPMode(safeInputs *SafeInputsConfig) bool { - return safeInputs != nil && (safeInputs.Mode == SafeInputsModeHTTP || safeInputs.Mode == "") + return safeInputs != nil } // IsSafeInputsEnabled checks if safe-inputs are configured. @@ -90,19 +85,12 @@ func IsSafeInputsEnabled(safeInputs *SafeInputsConfig, workflowData *WorkflowDat // Returns the config and a boolean indicating whether any tools were found. func parseSafeInputsMap(safeInputsMap map[string]any) (*SafeInputsConfig, bool) { config := &SafeInputsConfig{ - Mode: "http", // Default to HTTP mode + Mode: "http", // Only HTTP mode is supported Tools: make(map[string]*SafeInputToolConfig), } - // Parse mode if specified (optional field) - if mode, exists := safeInputsMap["mode"]; exists { - if modeStr, ok := mode.(string); ok { - // Validate mode value - if modeStr == "stdio" || modeStr == "http" { - config.Mode = modeStr - } - } - } + // Mode field is ignored - only HTTP mode is supported + // All safe-inputs configurations use HTTP transport for toolName, toolValue := range safeInputsMap { // Skip the "mode" field as it's not a tool definition @@ -387,37 +375,12 @@ func generateSafeInputsToolsConfig(safeInputs *SafeInputsConfig) string { } // generateSafeInputsMCPServerScript generates the entry point script for the safe-inputs MCP server -// This script chooses the transport based on mode: HTTP or stdio +// This script uses HTTP transport exclusively func generateSafeInputsMCPServerScript(safeInputs *SafeInputsConfig) string { var sb strings.Builder - if IsSafeInputsStdioMode(safeInputs) { - // Stdio transport - server started by agent - sb.WriteString(`// @ts-check -// Auto-generated safe-inputs MCP server entry point (stdio transport) -// This script uses the reusable safe_inputs_mcp_server module with stdio transport - -const path = require("path"); -const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); - -// Configuration file path (generated alongside this script) -const configPath = path.join(__dirname, "tools.json"); - -// Start the stdio server -// Note: skipCleanup is true for stdio mode to allow agent restarts -try { - startSafeInputsServer(configPath, { - logDir: "/tmp/gh-aw/safe-inputs/logs", - skipCleanup: true - }); -} catch (error) { - console.error("Failed to start safe-inputs stdio server:", error); - process.exit(1); -} -`) - } else { - // HTTP transport - server started in separate step - sb.WriteString(`// @ts-check + // HTTP transport - server started in separate step + sb.WriteString(`// @ts-check // Auto-generated safe-inputs MCP server entry point (HTTP transport) // This script uses the reusable safe_inputs_mcp_server_http module @@ -441,7 +404,6 @@ startHttpServer(configPath, { process.exit(1); }); `) - } return sb.String() } @@ -633,105 +595,65 @@ func collectSafeInputsSecrets(safeInputs *SafeInputsConfig) map[string]string { } // renderSafeInputsMCPConfigWithOptions generates the Safe Inputs MCP server configuration with engine-specific options -// Supports both HTTP and stdio transport modes +// Only supports HTTP transport mode func renderSafeInputsMCPConfigWithOptions(yaml *strings.Builder, safeInputs *SafeInputsConfig, isLast bool, includeCopilotFields bool) { envVars := getSafeInputsEnvVars(safeInputs) yaml.WriteString(" \"" + constants.SafeInputsMCPServerID + "\": {\n") - // Choose transport based on mode - if IsSafeInputsStdioMode(safeInputs) { - // Stdio transport configuration - server started by agent - // Use "local" for Copilot CLI, "stdio" for other engines - typeValue := "stdio" - if includeCopilotFields { - typeValue = "local" - } - yaml.WriteString(" \"type\": \"" + typeValue + "\",\n") - yaml.WriteString(" \"command\": \"node\",\n") - yaml.WriteString(" \"args\": [\"/tmp/gh-aw/safe-inputs/mcp-server.cjs\"],\n") - - // Add tools field for Copilot - if includeCopilotFields { - yaml.WriteString(" \"tools\": [\"*\"],\n") - } + // HTTP transport configuration - server started in separate step + // Add type field for HTTP (required by MCP specification for HTTP transport) + yaml.WriteString(" \"type\": \"http\",\n") - // Add env block for environment variable passthrough - yaml.WriteString(" \"env\": {\n") + // HTTP URL using environment variable + // Use host.docker.internal to allow access from firewall container + if includeCopilotFields { + // Copilot format: backslash-escaped shell variable reference + yaml.WriteString(" \"url\": \"http://host.docker.internal:\\${GH_AW_SAFE_INPUTS_PORT}\",\n") + } else { + // Claude/Custom format: direct shell variable reference + yaml.WriteString(" \"url\": \"http://host.docker.internal:$GH_AW_SAFE_INPUTS_PORT\",\n") + } - // Write environment variables with appropriate escaping - for i, envVar := range envVars { - isLastEnvVar := i == len(envVars)-1 - comma := "" - if !isLastEnvVar { - comma = "," - } + // Add Authorization header with API key + yaml.WriteString(" \"headers\": {\n") + if includeCopilotFields { + // Copilot format: backslash-escaped shell variable reference + yaml.WriteString(" \"Authorization\": \"Bearer \\${GH_AW_SAFE_INPUTS_API_KEY}\"\n") + } else { + // Claude/Custom format: direct shell variable reference + yaml.WriteString(" \"Authorization\": \"Bearer $GH_AW_SAFE_INPUTS_API_KEY\"\n") + } + yaml.WriteString(" },\n") - if includeCopilotFields { - // Copilot format: backslash-escaped shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n") - } else { - // Claude/Custom format: direct shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n") - } - } + // Add tools field for Copilot + if includeCopilotFields { + yaml.WriteString(" \"tools\": [\"*\"],\n") + } - yaml.WriteString(" }\n") - } else { - // HTTP transport configuration - server started in separate step - // Add type field for HTTP (required by MCP specification for HTTP transport) - yaml.WriteString(" \"type\": \"http\",\n") + // Add env block for environment variable passthrough + envVarsWithServerConfig := append([]string{"GH_AW_SAFE_INPUTS_PORT", "GH_AW_SAFE_INPUTS_API_KEY"}, envVars...) + yaml.WriteString(" \"env\": {\n") - // HTTP URL using environment variable - // Use host.docker.internal to allow access from firewall container - if includeCopilotFields { - // Copilot format: backslash-escaped shell variable reference - yaml.WriteString(" \"url\": \"http://host.docker.internal:\\${GH_AW_SAFE_INPUTS_PORT}\",\n") - } else { - // Claude/Custom format: direct shell variable reference - yaml.WriteString(" \"url\": \"http://host.docker.internal:$GH_AW_SAFE_INPUTS_PORT\",\n") + // Write environment variables with appropriate escaping + for i, envVar := range envVarsWithServerConfig { + isLastEnvVar := i == len(envVarsWithServerConfig)-1 + comma := "" + if !isLastEnvVar { + comma = "," } - // Add Authorization header with API key - yaml.WriteString(" \"headers\": {\n") if includeCopilotFields { // Copilot format: backslash-escaped shell variable reference - yaml.WriteString(" \"Authorization\": \"Bearer \\${GH_AW_SAFE_INPUTS_API_KEY}\"\n") + yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n") } else { // Claude/Custom format: direct shell variable reference - yaml.WriteString(" \"Authorization\": \"Bearer $GH_AW_SAFE_INPUTS_API_KEY\"\n") - } - yaml.WriteString(" },\n") - - // Add tools field for Copilot - if includeCopilotFields { - yaml.WriteString(" \"tools\": [\"*\"],\n") + yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n") } - - // Add env block for environment variable passthrough - envVarsWithServerConfig := append([]string{"GH_AW_SAFE_INPUTS_PORT", "GH_AW_SAFE_INPUTS_API_KEY"}, envVars...) - yaml.WriteString(" \"env\": {\n") - - // Write environment variables with appropriate escaping - for i, envVar := range envVarsWithServerConfig { - isLastEnvVar := i == len(envVarsWithServerConfig)-1 - comma := "" - if !isLastEnvVar { - comma = "," - } - - if includeCopilotFields { - // Copilot format: backslash-escaped shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n") - } else { - // Claude/Custom format: direct shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n") - } - } - - yaml.WriteString(" }\n") } + yaml.WriteString(" }\n") + if isLast { yaml.WriteString(" }\n") } else { @@ -753,21 +675,15 @@ func (c *Compiler) mergeSafeInputs(main *SafeInputsConfig, importedConfigs []str continue } - // Parse the imported JSON config + // Merge the imported JSON config var importedMap map[string]any if err := json.Unmarshal([]byte(configJSON), &importedMap); err != nil { safeInputsLog.Printf("Warning: failed to parse imported safe-inputs config: %v", err) continue } - // Merge mode if present in imported config and not set in main - if mode, exists := importedMap["mode"]; exists && main.Mode == "http" { - if modeStr, ok := mode.(string); ok { - if modeStr == "stdio" || modeStr == "http" { - main.Mode = modeStr - } - } - } + // Mode field is ignored - only HTTP mode is supported + // All safe-inputs configurations use HTTP transport // Merge each tool from the imported config for toolName, toolValue := range importedMap { diff --git a/pkg/workflow/safe_inputs_mode_test.go b/pkg/workflow/safe_inputs_mode_test.go index 947095ad7d..9fb2d60b3b 100644 --- a/pkg/workflow/safe_inputs_mode_test.go +++ b/pkg/workflow/safe_inputs_mode_test.go @@ -7,105 +7,6 @@ import ( "testing" ) -// TestSafeInputsStdioMode verifies that stdio mode generates correct configuration -func TestSafeInputsStdioMode(t *testing.T) { - // Create a temporary workflow file - tempDir := t.TempDir() - workflowPath := filepath.Join(tempDir, "test-workflow.md") - - workflowContent := `--- -on: workflow_dispatch -engine: copilot -safe-inputs: - mode: stdio - test-tool: - description: Test tool - script: | - return { result: "test" }; ---- - -Test safe-inputs stdio mode -` - - err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) - if err != nil { - t.Fatalf("Failed to write workflow file: %v", err) - } - - // Compile the workflow - compiler := NewCompiler(false, "", "test") - err = compiler.CompileWorkflow(workflowPath) - if err != nil { - t.Fatalf("Failed to compile workflow: %v", err) - } - - // Read the generated lock file - lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" - lockContent, err := os.ReadFile(lockPath) - if err != nil { - t.Fatalf("Failed to read lock file: %v", err) - } - - yamlStr := string(lockContent) - - // Verify that HTTP server startup steps are NOT present - unexpectedSteps := []string{ - "Generate Safe Inputs MCP Server Config", - "Start Safe Inputs MCP HTTP Server", - } - - for _, stepName := range unexpectedSteps { - if strings.Contains(yamlStr, stepName) { - t.Errorf("Unexpected HTTP server step found in stdio mode: %q", stepName) - } - } - - // Verify stdio configuration in MCP setup - if !strings.Contains(yamlStr, `"safeinputs"`) { - t.Error("Safe-inputs MCP server config not found") - } - - // Should use local transport for Copilot (stdio is converted to local for Copilot CLI compatibility) - if !strings.Contains(yamlStr, `"type": "local"`) { - t.Error("Expected type field set to 'local' in MCP config for Copilot engine") - } - - if !strings.Contains(yamlStr, `"command": "node"`) { - t.Error("Expected command field in stdio config") - } - - if !strings.Contains(yamlStr, `"/tmp/gh-aw/safe-inputs/mcp-server.cjs"`) { - t.Error("Expected mcp-server.cjs in args for stdio mode") - } - - // Should NOT have HTTP-specific fields - safeinputsConfig := extractSafeinputsConfigSection(yamlStr) - if strings.Contains(safeinputsConfig, `"url"`) { - t.Error("Stdio mode should not have URL field") - } - - if strings.Contains(safeinputsConfig, `"headers"`) { - t.Error("Stdio mode should not have headers field") - } - - // Verify the entry point script uses stdio - if !strings.Contains(yamlStr, "startSafeInputsServer") { - t.Error("Expected stdio entry point to use startSafeInputsServer") - } - - // Check the actual mcp-server.cjs entry point uses stdio server - entryPointSection := extractMCPServerEntryPoint(yamlStr) - if !strings.Contains(entryPointSection, "startSafeInputsServer(configPath") { - t.Error("Entry point should call startSafeInputsServer for stdio mode") - } - - if strings.Contains(entryPointSection, "startHttpServer") { - t.Error("Stdio mode entry point should not call startHttpServer") - } - - t.Logf("✓ Stdio mode correctly configured without HTTP server steps") -} - // TestSafeInputsHTTPMode verifies that HTTP mode generates correct configuration func TestSafeInputsHTTPMode(t *testing.T) { testCases := []struct { @@ -208,166 +109,11 @@ Test safe-inputs HTTP mode t.Error("Entry point should call startHttpServer for HTTP mode") } - if strings.Contains(entryPointSection, "startSafeInputsServer(configPath") { - t.Error("HTTP mode entry point should not call startSafeInputsServer") - } - t.Logf("✓ HTTP mode correctly configured with HTTP server steps") }) } } -// TestSafeInputsModeInImport verifies that mode can be set via imports -func TestSafeInputsModeInImport(t *testing.T) { - // Create a temporary directory structure - tempDir := t.TempDir() - sharedDir := filepath.Join(tempDir, "shared") - err := os.Mkdir(sharedDir, 0755) - if err != nil { - t.Fatalf("Failed to create shared directory: %v", err) - } - - // Create import file with stdio mode - importPath := filepath.Join(sharedDir, "tool.md") - importContent := `--- -safe-inputs: - mode: stdio - imported-tool: - description: Imported tool - script: | - return { result: "imported" }; ---- - -Imported tool -` - - err = os.WriteFile(importPath, []byte(importContent), 0644) - if err != nil { - t.Fatalf("Failed to write import file: %v", err) - } - - // Create main workflow that imports the tool - workflowPath := filepath.Join(tempDir, "workflow.md") - workflowContent := `--- -on: workflow_dispatch -engine: copilot -imports: - - shared/tool.md ---- - -Test mode via import -` - - err = os.WriteFile(workflowPath, []byte(workflowContent), 0644) - if err != nil { - t.Fatalf("Failed to write workflow file: %v", err) - } - - // Compile the workflow - compiler := NewCompiler(false, "", "test") - err = compiler.CompileWorkflow(workflowPath) - if err != nil { - t.Fatalf("Failed to compile workflow: %v", err) - } - - // Read the generated lock file - lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" - lockContent, err := os.ReadFile(lockPath) - if err != nil { - t.Fatalf("Failed to read lock file: %v", err) - } - - yamlStr := string(lockContent) - - // Verify local mode is used from import (converted from stdio for Copilot CLI) - if !strings.Contains(yamlStr, `"type": "local"`) { - t.Error("Expected local mode (converted from stdio) from imported configuration for Copilot engine") - } - - // Verify HTTP server steps are NOT present - if strings.Contains(yamlStr, "Start Safe Inputs MCP HTTP Server") { - t.Error("Should not have HTTP server step when mode is stdio via import") - } - - t.Logf("✓ Mode correctly inherited from import") -} - -// TestSafeInputsStdioModeWithClaudeEngine verifies that stdio mode with Claude engine uses "stdio" not "local" -func TestSafeInputsStdioModeWithClaudeEngine(t *testing.T) { - // Create a temporary workflow file - tempDir := t.TempDir() - workflowPath := filepath.Join(tempDir, "test-workflow.md") - - workflowContent := `--- -on: workflow_dispatch -engine: claude -safe-inputs: - mode: stdio - test-tool: - description: Test tool - script: | - return { result: "test" }; ---- - -Test safe-inputs stdio mode with Claude engine -` - - err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) - if err != nil { - t.Fatalf("Failed to write workflow file: %v", err) - } - - // Compile the workflow - compiler := NewCompiler(false, "", "test") - err = compiler.CompileWorkflow(workflowPath) - if err != nil { - t.Fatalf("Failed to compile workflow: %v", err) - } - - // Read the generated lock file - lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" - lockContent, err := os.ReadFile(lockPath) - if err != nil { - t.Fatalf("Failed to read lock file: %v", err) - } - - yamlStr := string(lockContent) - - // Verify that type field is "stdio" for Claude engine (not converted to "local") - if !strings.Contains(yamlStr, `"type": "stdio"`) { - t.Error("Expected type field set to 'stdio' in MCP config for Claude engine") - } - - // Verify "local" is NOT used - safeinputsConfig := extractSafeinputsConfigSection(yamlStr) - if strings.Contains(safeinputsConfig, `"type": "local"`) { - t.Error("Claude engine should use 'stdio' type, not 'local'") - } - - t.Logf("✓ Stdio mode correctly uses 'stdio' type for Claude engine") -} - -// extractSafeinputsConfigSection extracts the safeinputs configuration section from the YAML -func extractSafeinputsConfigSection(yamlStr string) string { - start := strings.Index(yamlStr, `"safeinputs"`) - if start == -1 { - return "" - } - - // Find the closing brace for the safeinputs object - // This is a simple heuristic - we look for the next server or closing brace - end := strings.Index(yamlStr[start:], `},`) - if end == -1 { - end = strings.Index(yamlStr[start:], `}`) - } - - if end == -1 { - return yamlStr[start:] - } - - return yamlStr[start : start+end+1] -} - // extractMCPServerEntryPoint extracts the mcp-server.cjs entry point script from the YAML func extractMCPServerEntryPoint(yamlStr string) string { // Find the mcp-server.cjs section