diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index de931eb321..c9236cf7e7 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -3233,16 +3233,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -3262,101 +3258,9 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -3365,15 +3269,11 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", - "headers": { - "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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}" } }, diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index 8082e635bb..698d110ffc 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -1099,7 +1099,7 @@ jobs: result, }; } catch (error) { - const err = error; + const err = (error); return { jsonrpc: "2.0", id, diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 887ce44125..dad8032ef7 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -1728,16 +1728,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -1757,100 +1753,8 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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_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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -1858,14 +1762,10 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_INPUTS_PORT", - "headers": { - "Authorization": "Bearer $GH_AW_SAFE_INPUTS_API_KEY" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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" } } diff --git a/.github/workflows/shared/gh.md b/.github/workflows/shared/gh.md index dc7fa3200c..044e56eaa7 100644 --- a/.github/workflows/shared/gh.md +++ b/.github/workflows/shared/gh.md @@ -1,5 +1,6 @@ --- safe-inputs: + mode: stdio gh: description: "Execute any gh CLI command. Provide the full command after 'gh' (e.g., args: 'pr list --limit 5'). The tool will run: gh . Use single quotes ' for complex args to avoid shell interpretation issues." inputs: diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index c427811d6f..07754075dc 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -4732,16 +4732,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -4761,102 +4757,10 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -4891,15 +4795,11 @@ jobs: "tools": ["*"] }, "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", - "headers": { - "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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}" } }, diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml index 4c1953edb6..158ec85957 100644 --- a/.github/workflows/smoke-copilot-playwright.lock.yml +++ b/.github/workflows/smoke-copilot-playwright.lock.yml @@ -4731,16 +4731,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -4760,102 +4756,10 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -4890,15 +4794,11 @@ jobs: "tools": ["*"] }, "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", - "headers": { - "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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}" } }, diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml index bf5203b339..a39bd73150 100644 --- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml +++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml @@ -4636,16 +4636,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -4665,101 +4661,9 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -4768,15 +4672,11 @@ jobs: { "mcpServers": { "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", - "headers": { - "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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}" } }, diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index aa316f9487..4691ea0716 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -4646,16 +4646,12 @@ jobs: EOF_TOOLS_JSON cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs << 'EOFSI' const path = require("path"); - const { startHttpServer } = require("./safe_inputs_mcp_server_http.cjs"); + const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); const configPath = path.join(__dirname, "tools.json"); - 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, + startSafeInputsServer(configPath, { logDir: "/tmp/gh-aw/safe-inputs/logs" }).catch(error => { - console.error("Failed to start safe-inputs HTTP server:", error); + console.error("Failed to start safe-inputs stdio server:", error); process.exit(1); }); EOFSI @@ -4675,102 +4671,10 @@ 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(32); - 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}" - - 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" > /tmp/gh-aw/safe-inputs/logs/server.log - echo "Start time: $(date)" >> /tmp/gh-aw/safe-inputs/logs/server.log - echo "===========================================" >> /tmp/gh-aw/safe-inputs/logs/server.log - 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: $(ps aux | grep '[m]cp-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 }} run: | mkdir -p /tmp/gh-aw/mcp-config @@ -4799,15 +4703,11 @@ jobs: } }, "safeinputs": { - "type": "http", - "url": "http://host.docker.internal:\${GH_AW_SAFE_INPUTS_PORT}", - "headers": { - "Authorization": "Bearer \${GH_AW_SAFE_INPUTS_API_KEY}" - }, + "type": "stdio", + "command": "node", + "args": ["/tmp/gh-aw/safe-inputs/mcp-server.cjs"], "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}" } }, diff --git a/pkg/parser/schemas/included_file_schema.json b/pkg/parser/schemas/included_file_schema.json index 008442b449..149838ac0a 100644 --- a/pkg/parser/schemas/included_file_schema.json +++ b/pkg/parser/schemas/included_file_schema.json @@ -567,6 +567,14 @@ }, "required": ["description"], "additionalProperties": false + }, + "properties": { + "mode": { + "type": "string", + "enum": ["http", "stdio"], + "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." + } } }, "secret-masking": { diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 743ae111ed..80de2d9f1e 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2073,7 +2073,7 @@ [ { "name": "Verify Post-Steps Execution", - "run": "echo \"✅ Post-steps are executing correctly\"\necho \"This step runs after the AI agent completes\"\n" + "run": "echo \"\u2705 Post-steps are executing correctly\"\necho \"This step runs after the AI agent completes\"\n" }, { "name": "Upload Test Results", @@ -4381,8 +4381,8 @@ }, "staged-title": { "type": "string", - "description": "Custom title template for staged mode preview. Available placeholders: {operation}. Example: '🎭 Preview: {operation}'", - "examples": ["🎭 Preview: {operation}", "## Staged Mode: {operation}"] + "description": "Custom title template for staged mode preview. Available placeholders: {operation}. Example: '\ud83c\udfad Preview: {operation}'", + "examples": ["\ud83c\udfad Preview: {operation}", "## Staged Mode: {operation}"] }, "staged-description": { "type": "string", @@ -4399,23 +4399,26 @@ }, "run-success": { "type": "string", - "description": "Custom message template for successful workflow completion. Available placeholders: {workflow_name}, {run_url}. Default: '✅ Agentic [{workflow_name}]({run_url}) completed successfully.'", - "examples": ["✅ Agentic [{workflow_name}]({run_url}) completed successfully.", "✅ [{workflow_name}]({run_url}) finished."] + "description": "Custom message template for successful workflow completion. Available placeholders: {workflow_name}, {run_url}. Default: '\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.'", + "examples": [ + "\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.", + "\u2705 [{workflow_name}]({run_url}) finished." + ] }, "run-failure": { "type": "string", - "description": "Custom message template for failed workflow. Available placeholders: {workflow_name}, {run_url}, {status}. Default: '❌ Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.'", + "description": "Custom message template for failed workflow. Available placeholders: {workflow_name}, {run_url}, {status}. Default: '\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.'", "examples": [ - "❌ Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", - "❌ [{workflow_name}]({run_url}) {status}." + "\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", + "\u274c [{workflow_name}]({run_url}) {status}." ] }, "detection-failure": { "type": "string", - "description": "Custom message template for detection job failure. Available placeholders: {workflow_name}, {run_url}. Default: '⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.'", + "description": "Custom message template for detection job failure. Available placeholders: {workflow_name}, {run_url}. Default: '\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.'", "examples": [ - "⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", - "⚠️ Detection job failed in [{workflow_name}]({run_url})." + "\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", + "\u26a0\ufe0f Detection job failed in [{workflow_name}]({run_url})." ] } }, @@ -4451,12 +4454,12 @@ "additionalProperties": false }, "roles": { - "description": "Repository access roles required to trigger agentic workflows. Defaults to ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any authenticated user (⚠️ security consideration).", + "description": "Repository access roles required to trigger agentic workflows. Defaults to ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any authenticated user (\u26a0\ufe0f security consideration).", "oneOf": [ { "type": "string", "enum": ["all"], - "description": "Allow any authenticated user to trigger the workflow (⚠️ disables permission checking entirely - use with caution)" + "description": "Allow any authenticated user to trigger the workflow (\u26a0\ufe0f disables permission checking entirely - use with caution)" }, { "type": "array", @@ -4546,7 +4549,7 @@ "type": "object", "description": "Safe inputs configuration for defining custom lightweight MCP tools as JavaScript, shell scripts, or Python scripts. Tools are mounted in an MCP server and have access to secrets specified by the user. Only one of 'script' (JavaScript), 'run' (shell), or 'py' (Python) must be specified per tool.", "patternProperties": { - "^[a-z][a-z0-9_-]*$": { + "^([a-ln-z][a-z0-9_-]*|m[a-np-z][a-z0-9_-]*|mo[a-ce-z][a-z0-9_-]*|mod[a-df-z][a-z0-9_-]*|mode[a-z0-9_-]+)$": { "type": "object", "description": "Custom tool definition. The key is the tool name (lowercase alphanumeric with dashes/underscores).", "required": ["description"], @@ -4660,7 +4663,6 @@ ] } }, - "additionalProperties": false, "examples": [ { "search-issues": { @@ -4699,7 +4701,15 @@ } } } - ] + ], + "properties": { + "mode": { + "type": "string", + "enum": ["http", "stdio"], + "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." + } + } }, "runtimes": { "type": "object", diff --git a/pkg/workflow/js/mcp_server_core.cjs b/pkg/workflow/js/mcp_server_core.cjs index 8e8ddc22bf..824e8bbd6e 100644 --- a/pkg/workflow/js/mcp_server_core.cjs +++ b/pkg/workflow/js/mcp_server_core.cjs @@ -579,7 +579,7 @@ async function handleRequest(server, request, defaultHandler) { result, }; } catch (error) { - const err = /** @type {any} */ (error); + const err = /** @type {any} */ error; return { jsonrpc: "2.0", id, diff --git a/pkg/workflow/mcp_servers.go b/pkg/workflow/mcp_servers.go index 38edb0a696..3033387d53 100644 --- a/pkg/workflow/mcp_servers.go +++ b/pkg/workflow/mcp_servers.go @@ -276,6 +276,7 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, } // Write safe-inputs MCP server if configured and feature flag is enabled + // For stdio mode, we only write the files but don't start the HTTP server if IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) { // Step 1: Write JavaScript and config files yaml.WriteString(" - name: Setup Safe Inputs JavaScript and Config\n") @@ -420,43 +421,46 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, } 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") - - // 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") + // 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") - // 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)) + // Use the embedded shell script to start the server + WriteShellScriptToYAML(yaml, startSafeInputsServerScript, " ") + yaml.WriteString(" \n") } - yaml.WriteString(" \n") - - // Use the embedded shell script to start the server - WriteShellScriptToYAML(yaml, startSafeInputsServerScript, " ") - yaml.WriteString(" \n") } // Use the engine's RenderMCPConfig method @@ -524,13 +528,15 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, } } - // Add safe-inputs env vars if present (for secrets passthrough and server config) + // Add safe-inputs env vars if present if hasSafeInputs { - // 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 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 tool-specific env vars (secrets passthrough) + // Add tool-specific env vars (secrets passthrough) - needed for both modes 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 aa9d956e79..3bd759771e 100644 --- a/pkg/workflow/safe_inputs.go +++ b/pkg/workflow/safe_inputs.go @@ -33,6 +33,7 @@ func sanitizeParameterName(name string) string { // SafeInputsConfig holds the configuration for safe-inputs custom tools type SafeInputsConfig struct { + Mode string // Transport mode: "http" (default) or "stdio" Tools map[string]*SafeInputToolConfig } @@ -59,11 +60,27 @@ type SafeInputParam struct { // SafeInputsFeatureFlag is the name of the feature flag for safe-inputs const SafeInputsFeatureFlag = "safe-inputs" +// SafeInputsMode constants define the available transport modes +const ( + SafeInputsModeHTTP = "http" + SafeInputsModeStdio = "stdio" +) + // HasSafeInputs checks if safe-inputs are configured 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 +func IsSafeInputsHTTPMode(safeInputs *SafeInputsConfig) bool { + return safeInputs != nil && (safeInputs.Mode == SafeInputsModeHTTP || safeInputs.Mode == "") +} + // IsSafeInputsEnabled checks if safe-inputs are configured. // Safe-inputs are enabled by default when configured in the workflow. // The workflowData parameter is kept for backward compatibility but is not used. @@ -76,10 +93,26 @@ 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 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 + } + } + } + for toolName, toolValue := range safeInputsMap { + // Skip the "mode" field as it's not a tool definition + if toolName == "mode" { + continue + } + toolMap, ok := toolValue.(map[string]any) if !ok { continue @@ -357,12 +390,33 @@ func generateSafeInputsToolsConfig(safeInputs *SafeInputsConfig) string { } // generateSafeInputsMCPServerScript generates the entry point script for the safe-inputs MCP server -// This uses the reusable safe_inputs_mcp_server_http.cjs module and reads tool configuration from tools.json +// This script chooses the transport based on mode: HTTP or stdio func generateSafeInputsMCPServerScript(safeInputs *SafeInputsConfig) string { var sb strings.Builder - // Write a simple entry point that uses the HTTP modular MCP server - sb.WriteString(`// @ts-check + 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 +startSafeInputsServer(configPath, { + logDir: "/tmp/gh-aw/safe-inputs/logs" +}).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 // Auto-generated safe-inputs MCP server entry point (HTTP transport) // This script uses the reusable safe_inputs_mcp_server_http module @@ -386,6 +440,7 @@ startHttpServer(configPath, { process.exit(1); }); `) + } return sb.String() } @@ -577,64 +632,99 @@ func collectSafeInputsSecrets(safeInputs *SafeInputsConfig) map[string]string { } // renderSafeInputsMCPConfigWithOptions generates the Safe Inputs MCP server configuration with engine-specific options -// Now uses HTTP transport instead of stdio +// Supports both HTTP and stdio transport modes func renderSafeInputsMCPConfigWithOptions(yaml *strings.Builder, safeInputs *SafeInputsConfig, isLast bool, includeCopilotFields bool) { envVars := getSafeInputsEnvVars(safeInputs) - // Use HTTP transport configuration yaml.WriteString(" \"" + constants.SafeInputsMCPServerID + "\": {\n") - // Add type field for HTTP (required by MCP specification for HTTP transport) - yaml.WriteString(" \"type\": \"http\",\n") + // Choose transport based on mode + if IsSafeInputsStdioMode(safeInputs) { + // Stdio transport configuration - server started by agent + yaml.WriteString(" \"type\": \"stdio\",\n") + yaml.WriteString(" \"command\": \"node\",\n") + yaml.WriteString(" \"args\": [\"/tmp/gh-aw/safe-inputs/mcp-server.cjs\"],\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") - } + // Add tools field for Copilot + if includeCopilotFields { + yaml.WriteString(" \"tools\": [\"*\"],\n") + } - // 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") + // Add env block for environment variable passthrough + yaml.WriteString(" \"env\": {\n") - // Add tools field for Copilot - if includeCopilotFields { - yaml.WriteString(" \"tools\": [\"*\"],\n") - } + // Write environment variables with appropriate escaping + for i, envVar := range envVars { + isLastEnvVar := i == len(envVars)-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") + } + } - // 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") + 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") - // Write environment variables with appropriate escaping - for i, envVar := range envVarsWithServerConfig { - isLastEnvVar := i == len(envVarsWithServerConfig)-1 - comma := "" - if !isLastEnvVar { - comma = "," + // 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") } + // Add Authorization header with API key + yaml.WriteString(" \"headers\": {\n") if includeCopilotFields { // Copilot format: backslash-escaped shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n") + yaml.WriteString(" \"Authorization\": \"Bearer \\${GH_AW_SAFE_INPUTS_API_KEY}\"\n") } else { // Claude/Custom format: direct shell variable reference - yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n") + 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(" }\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") + } if isLast { yaml.WriteString(" }\n") @@ -647,6 +737,7 @@ func renderSafeInputsMCPConfigWithOptions(yaml *strings.Builder, safeInputs *Saf func (c *Compiler) mergeSafeInputs(main *SafeInputsConfig, importedConfigs []string) *SafeInputsConfig { if main == nil { main = &SafeInputsConfig{ + Mode: "http", // Default to HTTP mode Tools: make(map[string]*SafeInputToolConfig), } } @@ -663,8 +754,22 @@ func (c *Compiler) mergeSafeInputs(main *SafeInputsConfig, importedConfigs []str 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 + } + } + } + // Merge each tool from the imported config for toolName, toolValue := range importedMap { + // Skip mode field as it's already handled + if toolName == "mode" { + continue + } + // Skip if tool already exists in main config (main takes precedence) if _, exists := main.Tools[toolName]; exists { safeInputsLog.Printf("Skipping imported tool '%s' - already defined in main config", toolName) diff --git a/pkg/workflow/safe_inputs_mode_test.go b/pkg/workflow/safe_inputs_mode_test.go new file mode 100644 index 0000000000..ea3064843e --- /dev/null +++ b/pkg/workflow/safe_inputs_mode_test.go @@ -0,0 +1,331 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "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 stdio transport + if !strings.Contains(yamlStr, `"type": "stdio"`) { + t.Error("Expected type field set to 'stdio' in MCP config") + } + + 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 { + name string + mode string // empty string tests default behavior + }{ + { + name: "explicit_http_mode", + mode: "http", + }, + { + name: "default_mode", + mode: "", // No mode specified, should default to HTTP + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a temporary workflow file + tempDir := t.TempDir() + workflowPath := filepath.Join(tempDir, "test-workflow.md") + + modeField := "" + if tc.mode != "" { + modeField = " mode: " + tc.mode + "\n" + } + + workflowContent := `--- +on: workflow_dispatch +engine: copilot +safe-inputs: +` + modeField + ` test-tool: + description: Test tool + script: | + return { result: "test" }; +--- + +Test safe-inputs HTTP 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 present + expectedSteps := []string{ + "Generate Safe Inputs MCP Server Config", + "Start Safe Inputs MCP HTTP Server", + } + + for _, stepName := range expectedSteps { + if !strings.Contains(yamlStr, stepName) { + t.Errorf("Expected HTTP server step not found: %q", stepName) + } + } + + // Verify HTTP configuration in MCP setup + if !strings.Contains(yamlStr, `"safeinputs"`) { + t.Error("Safe-inputs MCP server config not found") + } + + // Should use HTTP transport + if !strings.Contains(yamlStr, `"type": "http"`) { + t.Error("Expected type field set to 'http' in MCP config") + } + + if !strings.Contains(yamlStr, `"url": "http://host.docker.internal`) { + t.Error("Expected HTTP URL in config") + } + + if !strings.Contains(yamlStr, `"headers"`) { + t.Error("Expected headers field in HTTP config") + } + + // Verify the entry point script uses HTTP + if !strings.Contains(yamlStr, "startHttpServer") { + t.Error("Expected HTTP entry point to use startHttpServer") + } + + // Check the actual mcp-server.cjs entry point uses HTTP server + entryPointSection := extractMCPServerEntryPoint(yamlStr) + if !strings.Contains(entryPointSection, "startHttpServer(configPath") { + 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 stdio mode is used from import + if !strings.Contains(yamlStr, `"type": "stdio"`) { + t.Error("Expected stdio mode from imported configuration") + } + + // 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") +} + +// 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 + start := strings.Index(yamlStr, "cat > /tmp/gh-aw/safe-inputs/mcp-server.cjs") + if start == -1 { + return "" + } + + // Find the EOFSI marker that ends the heredoc + end := strings.Index(yamlStr[start:], "EOFSI") + if end == -1 { + return "" + } + + return yamlStr[start : start+end] +}