diff --git a/.env.example b/.env.example index 4c01091b..5e5a3d45 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,41 @@ ANTHROPIC_API_KEY=your-api-key-here # AWS_REGION=us-east-1 # AWS_BEARER_TOKEN_BEDROCK=your-bearer-token +# ============================================================================= +# OPTION 4: GitHub Copilot +# ============================================================================= +# Uses @github/copilot-sdk to access AI models via your GitHub Copilot subscription. +# No Anthropic API key required — just a GitHub token from a Copilot-enabled account. +# +# Quick setup: +# ./shannon login # Interactive login (uses gh CLI or manual token entry) +# ./shannon models # List all available models +# +# Or set your token manually (PAT with 'copilot' scope, or GH CLI token): +# GITHUB_TOKEN=ghp_your-github-token-here +# +# Alternatively: +# GH_TOKEN=ghp_your-github-token-here +# COPILOT_GITHUB_TOKEN=ghp_your-github-token-here +# +# Force Copilot mode (auto-detected when GITHUB_TOKEN set and no ANTHROPIC_API_KEY): +# COPILOT_PROVIDER=true +# +# Override Copilot model selection (Claude uses Copilot SDK; GPT/Gemini/Grok use +# the GitHub Models OpenAI-compatible API — both paths work automatically): +# COPILOT_SMALL_MODEL=claude-haiku-4.5 +# COPILOT_MEDIUM_MODEL=claude-sonnet-4.6 +# COPILOT_LARGE_MODEL=claude-opus-4.6 +# +# Available models (run ./shannon models for full list): +# Claude: claude-haiku-4.5, claude-sonnet-4, claude-sonnet-4.5, claude-sonnet-4.6, +# claude-opus-4.1, claude-opus-4.5, claude-opus-4.6 +# GPT: gpt-4o, gpt-4.1, gpt-5, gpt-5-mini, gpt-5.1, gpt-5.1-codex, +# gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2, gpt-5.2-codex +# Gemini: gemini-2.5-pro, gemini-3-flash-preview, gemini-3-pro-preview, +# gemini-3.1-pro-preview +# Other: grok-code-fast-1 + # ============================================================================= # OPTION 4: Google Vertex AI # ============================================================================= diff --git a/Dockerfile b/Dockerfile index a78a2108..831bbc75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -129,6 +129,9 @@ RUN npm prune --production && \ RUN npm install -g @anthropic-ai/claude-code +# Install GitHub Copilot CLI (required by @github/copilot-sdk for Copilot provider mode) +RUN npm install -g @github/copilot 2>/dev/null || true + # Create directories for session data and ensure proper permissions RUN mkdir -p /app/sessions /app/deliverables /app/repos /app/configs && \ mkdir -p /tmp/.cache /tmp/.config /tmp/.npm && \ diff --git a/docker-compose.yml b/docker-compose.yml index 843e7522..6387cafd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,15 @@ services: - ANTHROPIC_MEDIUM_MODEL=${ANTHROPIC_MEDIUM_MODEL:-} - ANTHROPIC_LARGE_MODEL=${ANTHROPIC_LARGE_MODEL:-} - CLAUDE_CODE_MAX_OUTPUT_TOKENS=${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-64000} + # GitHub Copilot provider + - COPILOT_PROVIDER=${COPILOT_PROVIDER:-} + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GH_TOKEN=${GH_TOKEN:-} + - COPILOT_GITHUB_TOKEN=${COPILOT_GITHUB_TOKEN:-} + - COPILOT_SMALL_MODEL=${COPILOT_SMALL_MODEL:-} + - COPILOT_MEDIUM_MODEL=${COPILOT_MEDIUM_MODEL:-} + - COPILOT_LARGE_MODEL=${COPILOT_LARGE_MODEL:-} + - COPILOT_LOG_LEVEL=${COPILOT_LOG_LEVEL:-warning} depends_on: temporal: condition: service_healthy diff --git a/package-lock.json b/package-lock.json index 9d2b9807..9211206f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.38", + "@github/copilot-sdk": "^0.1.30", "@temporalio/activity": "^1.11.0", "@temporalio/client": "^1.11.0", "@temporalio/worker": "^1.11.0", @@ -51,6 +52,133 @@ "zod": "^4.0.0" } }, + "node_modules/@github/copilot": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.420.tgz", + "integrity": "sha512-UpPuSjxUxQ+j02WjZEFffWf0scLb23LvuGHzMFtaSsweR+P/BdbtDUI5ZDIA6T0tVyyt6+X1/vgfsJiRqd6jig==", + "license": "SEE LICENSE IN LICENSE.md", + "bin": { + "copilot": "npm-loader.js" + }, + "optionalDependencies": { + "@github/copilot-darwin-arm64": "0.0.420", + "@github/copilot-darwin-x64": "0.0.420", + "@github/copilot-linux-arm64": "0.0.420", + "@github/copilot-linux-x64": "0.0.420", + "@github/copilot-win32-arm64": "0.0.420", + "@github/copilot-win32-x64": "0.0.420" + } + }, + "node_modules/@github/copilot-darwin-arm64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.420.tgz", + "integrity": "sha512-sj8Oxcf3oKDbeUotm2gtq5YU1lwCt3QIzbMZioFD/PMLOeqSX/wrecI+c0DDYXKofFhALb0+DxxnWgbEs0mnkQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-arm64": "copilot" + } + }, + "node_modules/@github/copilot-darwin-x64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.420.tgz", + "integrity": "sha512-2acA93IqXz1uuz3TVUm0Y7BVrBr0MySh1kQa8LqMILhTsG0YHRMm8ybzTp2HA7Mi1tl5CjqMSk163kkS7OzfUA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-x64": "copilot" + } + }, + "node_modules/@github/copilot-linux-arm64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.420.tgz", + "integrity": "sha512-h/IvEryTOYm1HzR2GNq8s2aDtN4lvT4MxldfZuS42CtWJDOfVG2jLLsoHWU1T3QV8j1++PmDgE//HX0JLpLMww==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-arm64": "copilot" + } + }, + "node_modules/@github/copilot-linux-x64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.420.tgz", + "integrity": "sha512-iL2NpZvXIDZ+3lw7sO2fo5T0nKmP5dZbU2gdYcv+SFBm/ONhCxIY5VRX4yN/9VkFaa9ePv5JzCnsl3vZINiDxg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-x64": "copilot" + } + }, + "node_modules/@github/copilot-sdk": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.30.tgz", + "integrity": "sha512-Stg+h8xsPRR0TNGBQfd9laxhJfWZ6DsdpbowcKIZoyKxZvMAbjnY0zyDeOpewJbxWBTJVhBZb5okOq6iaPNMZw==", + "license": "MIT", + "dependencies": { + "@github/copilot": "^0.0.420", + "vscode-jsonrpc": "^8.2.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@github/copilot-win32-arm64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.420.tgz", + "integrity": "sha512-Njlc2j9vYSBAL+lC6FIEhQ3C+VxO3xavwKnw0ecVRiNLcGLyPrTdzPfPQOmEjC63gpVCqLabikoDGv8fuLPA2w==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-arm64": "copilot.exe" + } + }, + "node_modules/@github/copilot-win32-x64": { + "version": "0.0.420", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.420.tgz", + "integrity": "sha512-rZlH35oNehAP2DvQbu4vQFVNeCh/1p3rUjafBYaEY0Nkhx7RmdrYBileL5U3PtRPPRsBPaq3Qp+pVIrGoCDLzQ==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-x64": "copilot.exe" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", @@ -998,6 +1126,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/tinycolor2": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", @@ -1179,7 +1317,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, + "peer":true, "bin": { "acorn": "bin/acorn" }, @@ -1199,6 +1337,18 @@ "acorn": "^8.14.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1325,6 +1475,12 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.14", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", @@ -1396,6 +1552,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", @@ -1566,6 +1735,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", @@ -1575,6 +1756,15 @@ "node": ">=20" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1587,6 +1777,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -1612,12 +1816,57 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1725,12 +1974,56 @@ "node": ">= 17.0.0" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/fs-monkey": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", "license": "Unlicense" }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1752,6 +2045,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-to-regex.js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", @@ -1774,6 +2104,18 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1802,6 +2144,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/heap-js": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", @@ -1811,6 +2192,21 @@ "node": ">=10.0.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/humanize-ms/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -1904,6 +2300,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memfs": { "version": "4.51.1", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", @@ -1973,6 +2378,46 @@ "node": ">= 18.0.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2331,6 +2776,12 @@ "tinycolor2": "^1.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-dump": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", @@ -2351,8 +2802,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-fest": { "version": "4.41.0", @@ -2437,6 +2887,15 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/watchpack": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", @@ -2450,6 +2909,21 @@ "node": ">=10.13.0" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.104.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", @@ -2508,6 +2982,16 @@ "node": ">=10.13.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", diff --git a/package.json b/package.json index 8758b3f9..9240390f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.38", + "@github/copilot-sdk": "^0.1.30", "@temporalio/activity": "^1.11.0", "@temporalio/client": "^1.11.0", "@temporalio/worker": "^1.11.0", diff --git a/shannon b/shannon index 0a96eba3..27c9bf48 100755 --- a/shannon +++ b/shannon @@ -41,6 +41,8 @@ show_help() { Usage: ./shannon start URL= REPO= Start a pentest workflow + ./shannon login Authenticate with GitHub Copilot + ./shannon models List available Copilot models ./shannon workspaces List all workspaces ./shannon logs ID= Tail logs for a specific workflow ./shannon stop Stop all containers @@ -144,7 +146,17 @@ cmd_start() { # Check for API key (Bedrock and router modes can bypass this) if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then - if [ "$CLAUDE_CODE_USE_BEDROCK" = "1" ]; then + if [ -n "$GITHUB_TOKEN" ] || [ -n "$GH_TOKEN" ] || [ -n "$COPILOT_GITHUB_TOKEN" ] || [ "$COPILOT_PROVIDER" = "true" ]; then + # GitHub Copilot mode — validate token is present + if [ -z "$GITHUB_TOKEN" ] && [ -z "$GH_TOKEN" ] && [ -z "$COPILOT_GITHUB_TOKEN" ]; then + echo "ERROR: Copilot mode requires a GitHub token." + echo " Set GITHUB_TOKEN, GH_TOKEN, or COPILOT_GITHUB_TOKEN in .env" + exit 1 + fi + export COPILOT_PROVIDER=true + # Set a placeholder so SDK init doesn't fail on missing ANTHROPIC_API_KEY + export ANTHROPIC_API_KEY="copilot-mode" + elif [ "$CLAUDE_CODE_USE_BEDROCK" = "1" ]; then # Bedrock mode — validate required AWS credentials MISSING="" [ -z "$AWS_REGION" ] && MISSING="$MISSING AWS_REGION" @@ -185,7 +197,8 @@ cmd_start() { export ANTHROPIC_API_KEY="router-mode" else echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" - echo " (or use CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock," + echo " (or use GITHUB_TOKEN for Copilot mode," + echo " CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock," echo " CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI," echo " or ROUTER=true with OPENAI_API_KEY or OPENROUTER_API_KEY)" exit 1 @@ -351,12 +364,212 @@ cmd_stop() { fi } +cmd_login() { + echo "" + echo " GitHub Copilot Login" + echo " ====================" + echo "" + echo " Shannon can use GitHub Copilot to access AI models (Claude, GPT, Gemini)" + echo " without needing a separate Anthropic API key." + echo "" + + # Check if already configured + if [ -f .env ]; then + set -a; source .env 2>/dev/null; set +a + EXISTING_TOKEN="${COPILOT_GITHUB_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}" + if [ -n "$EXISTING_TOKEN" ] && [ "$EXISTING_TOKEN" != "ghp_your-github-token-here" ]; then + echo " A GitHub token is already configured in .env" + printf " Do you want to replace it? [y/N] " + read -r REPLACE + if [ "$REPLACE" != "y" ] && [ "$REPLACE" != "Y" ]; then + echo " Keeping existing token." + return 0 + fi + fi + fi + + # Method 1: gh CLI (preferred) + if command -v gh &>/dev/null; then + echo " Found GitHub CLI (gh). Logging in..." + echo "" + + # Check if already authenticated + if gh auth status &>/dev/null; then + echo " Already authenticated with GitHub CLI." + printf " Re-authenticate with Copilot scope? [Y/n] " + read -r REAUTH + if [ "$REAUTH" = "n" ] || [ "$REAUTH" = "N" ]; then + # Just extract existing token + TOKEN=$(gh auth token 2>/dev/null) + if [ -n "$TOKEN" ]; then + save_github_token "$TOKEN" + return 0 + fi + fi + fi + + # Login with copilot scope + echo " This will open a browser for GitHub authentication." + echo "" + if gh auth login --scopes "copilot" --web; then + TOKEN=$(gh auth token 2>/dev/null) + if [ -n "$TOKEN" ]; then + save_github_token "$TOKEN" + echo "" + echo " Login successful! You can now run:" + echo " ./shannon start URL= REPO=" + return 0 + else + echo " ERROR: Login succeeded but could not extract token." + echo " Run 'gh auth token' manually and add GITHUB_TOKEN to .env" + return 1 + fi + else + echo "" + echo " gh auth login failed. Falling back to manual method..." + echo "" + fi + fi + + # Method 2: Manual PAT creation + echo " No GitHub CLI found (or login failed). Create a token manually:" + echo "" + echo " 1. Go to: https://github.com/settings/tokens?type=beta" + echo " 2. Click 'Generate new token'" + echo " 3. Give it a name (e.g. 'Shannon Pentest')" + echo " 4. Under 'Permissions', enable 'Copilot' access" + echo " 5. Click 'Generate token' and copy the token" + echo "" + echo " Or for a classic token:" + echo " 1. Go to: https://github.com/settings/tokens/new" + echo " 2. Give it a name, set expiration" + echo " 3. Check the 'copilot' scope" + echo " 4. Click 'Generate token' and copy it" + echo "" + printf " Paste your GitHub token (input hidden): " + read -rs TOKEN + echo "" + + if [ -z "$TOKEN" ]; then + echo " ERROR: No token provided." + return 1 + fi + + # Basic token format validation + case "$TOKEN" in + ghp_*|gho_*|ghu_*|ghs_*|ghr_*|github_pat_*) + ;; + *) + echo " WARNING: Token doesn't match expected GitHub token format (ghp_*, github_pat_*, etc.)" + printf " Continue anyway? [y/N] " + read -r CONTINUE + if [ "$CONTINUE" != "y" ] && [ "$CONTINUE" != "Y" ]; then + echo " Aborted." + return 1 + fi + ;; + esac + + save_github_token "$TOKEN" + echo "" + echo " Token saved! You can now run:" + echo " ./shannon start URL= REPO=" +} + +save_github_token() { + local TOKEN="$1" + + # Create .env if it doesn't exist + if [ ! -f .env ]; then + cp .env.example .env 2>/dev/null || touch .env + fi + + # Update or add GITHUB_TOKEN in .env + if grep -q '^GITHUB_TOKEN=' .env 2>/dev/null; then + # Replace existing line (portable sed) + sed -i.bak "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$TOKEN|" .env && rm -f .env.bak + elif grep -q '^# GITHUB_TOKEN=' .env 2>/dev/null; then + # Uncomment and set + sed -i.bak "s|^# GITHUB_TOKEN=.*|GITHUB_TOKEN=$TOKEN|" .env && rm -f .env.bak + else + # Append + echo "" >> .env + echo "GITHUB_TOKEN=$TOKEN" >> .env + fi + + # Enable Copilot provider + if grep -q '^COPILOT_PROVIDER=' .env 2>/dev/null; then + sed -i.bak 's|^COPILOT_PROVIDER=.*|COPILOT_PROVIDER=true|' .env && rm -f .env.bak + elif grep -q '^# COPILOT_PROVIDER=' .env 2>/dev/null; then + sed -i.bak 's|^# COPILOT_PROVIDER=.*|COPILOT_PROVIDER=true|' .env && rm -f .env.bak + else + echo "COPILOT_PROVIDER=true" >> .env + fi + + # Comment out ANTHROPIC_API_KEY placeholder if it's still the default + if grep -q '^ANTHROPIC_API_KEY=your-api-key-here' .env 2>/dev/null; then + sed -i.bak 's|^ANTHROPIC_API_KEY=your-api-key-here|# ANTHROPIC_API_KEY=your-api-key-here|' .env && rm -f .env.bak + fi + + echo " Saved GITHUB_TOKEN to .env and enabled COPILOT_PROVIDER=true" +} + +cmd_models() { + echo "" + echo " Available Copilot Models" + echo " ========================" + echo "" + echo " Claude Models:" + echo " claude-haiku-4.5 Claude Haiku 4.5 (fast, small tasks)" + echo " claude-sonnet-4 Claude Sonnet 4" + echo " claude-sonnet-4.5 Claude Sonnet 4.5" + echo " claude-sonnet-4.6 Claude Sonnet 4.6 (default medium)" + echo " claude-opus-41 Claude Opus 4.1" + echo " claude-opus-4.5 Claude Opus 4.5" + echo " claude-opus-4.6 Claude Opus 4.6 (default large)" + echo "" + echo " GPT Models:" + echo " gpt-4o GPT-4o" + echo " gpt-4.1 GPT-4.1" + echo " gpt-5 GPT-5" + echo " gpt-5-mini GPT-5 Mini" + echo " gpt-5.1 GPT-5.1" + echo " gpt-5.1-codex GPT-5.1 Codex" + echo " gpt-5.1-codex-max GPT-5.1 Codex Max" + echo " gpt-5.1-codex-mini GPT-5.1 Codex Mini" + echo " gpt-5.2 GPT-5.2" + echo " gpt-5.2-codex GPT-5.2 Codex" + echo "" + echo " Gemini Models:" + echo " gemini-2.5-pro Gemini 2.5 Pro" + echo " gemini-3-flash-preview Gemini 3 Flash" + echo " gemini-3-pro-preview Gemini 3 Pro Preview" + echo " gemini-3.1-pro-preview Gemini 3.1 Pro Preview" + echo "" + echo " Other Models:" + echo " grok-code-fast-1 Grok Code Fast 1" + echo "" + echo " Override in .env:" + echo " COPILOT_SMALL_MODEL=claude-haiku-4.5" + echo " COPILOT_MEDIUM_MODEL=claude-sonnet-4.6" + echo " COPILOT_LARGE_MODEL=claude-opus-4.6" + echo "" +} + # Main command dispatch case "${1:-help}" in start) shift cmd_start "$@" ;; + login) + shift + cmd_login "$@" + ;; + models) + shift + cmd_models "$@" + ;; logs) shift cmd_logs "$@" diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index 04c990cb..2593b312 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -25,6 +25,7 @@ import { createProgressManager } from './progress-manager.js'; import { createAuditLogger } from './audit-logger.js'; import { getActualModelName } from './router-utils.js'; import { resolveModel, type ModelTier } from './models.js'; +import { isCopilotProvider, runCopilotPrompt } from './copilot-executor.js'; import type { ActivityLogger } from '../types/activity-logger.js'; declare global { @@ -196,6 +197,7 @@ export async function validateAgentOutput( // Low-level SDK execution. Handles message streaming, progress, and audit logging. // Exported for Temporal activities to call single-attempt execution. +// Routes to Copilot SDK when COPILOT_PROVIDER=true or GitHub token is the only credential. export async function runClaudePrompt( prompt: string, sourceDir: string, @@ -206,6 +208,11 @@ export async function runClaudePrompt( logger: ActivityLogger, modelTier: ModelTier = 'medium' ): Promise { + // 0. Route to Copilot SDK when configured + if (isCopilotProvider()) { + return runCopilotPrompt(prompt, sourceDir, context, description, agentName, auditSession, logger, modelTier); + } + // 1. Initialize timing and prompt const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`); const fullPrompt = context ? `${context}\n\n${prompt}` : prompt; diff --git a/src/ai/copilot-executor.ts b/src/ai/copilot-executor.ts new file mode 100644 index 00000000..b0bb4058 --- /dev/null +++ b/src/ai/copilot-executor.ts @@ -0,0 +1,407 @@ +// Copyright (C) 2025 Keygraph, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License version 3 +// as published by the Free Software Foundation. + +/** + * GitHub Copilot SDK executor. + * + * Provides an alternative execution path using @github/copilot-sdk instead of + * @anthropic-ai/claude-agent-sdk. Authenticates via GITHUB_TOKEN and accesses + * Claude models through the Copilot API. + * + * Returns the same ClaudePromptResult interface so the rest of the pipeline + * (agent-execution service, Temporal activities, validators) works unchanged. + */ + +import { CopilotClient, approveAll } from '@github/copilot-sdk'; +import type { + SessionConfig, + MCPLocalServerConfig, + MCPServerConfig, + SessionEvent, + Tool, +} from '@github/copilot-sdk'; +import { path, fs } from 'zx'; + +import { AGENTS, MCP_AGENT_MAPPING } from '../session-manager.js'; +import { AuditSession } from '../audit/index.js'; +import { Timer } from '../utils/metrics.js'; +import { isRetryableError, PentestError } from '../services/error-handling.js'; +import { isSpendingCapBehavior } from '../utils/billing-detection.js'; +import { resolveCopilotModel } from './models.js'; +import type { ModelTier } from './models.js'; +import type { ActivityLogger } from '../types/activity-logger.js'; +import type { ClaudePromptResult } from './claude-executor.js'; +import type { AgentName } from '../types/index.js'; + +// Singleton client — reused across agent executions within the same worker process +let sharedClient: CopilotClient | null = null; + +function getGitHubToken(): string | undefined { + return process.env.COPILOT_GITHUB_TOKEN + || process.env.GH_TOKEN + || process.env.GITHUB_TOKEN; +} + +/** Check whether the Copilot provider is configured. */ +export function isCopilotProvider(): boolean { + if (process.env.COPILOT_PROVIDER === 'true') { + return true; + } + // Auto-detect: if a GitHub token is set and no real Anthropic key, use Copilot + const hasGitHubToken = !!getGitHubToken(); + const apiKey = process.env.ANTHROPIC_API_KEY; + const hasRealAnthropicKey = !!( + (apiKey && apiKey !== 'copilot-mode' && apiKey !== 'router-mode') + || process.env.CLAUDE_CODE_OAUTH_TOKEN + ); + return hasGitHubToken && !hasRealAnthropicKey; +} + +async function getOrCreateClient(logger: ActivityLogger): Promise { + if (sharedClient) { + const state = sharedClient.getState(); + if (state === 'connected') { + return sharedClient; + } + // Stale connection — clean up and recreate + try { await sharedClient.stop(); } catch { /* best effort */ } + sharedClient = null; + } + + const token = getGitHubToken(); + if (!token) { + throw new PentestError( + 'GitHub Copilot requires a token. Set GITHUB_TOKEN, GH_TOKEN, or COPILOT_GITHUB_TOKEN.', + 'config', + false, + ); + } + + logger.info('Initializing GitHub Copilot SDK client...'); + + const client = new CopilotClient({ + githubToken: token, + useStdio: true, + autoStart: true, + autoRestart: true, + logLevel: process.env.COPILOT_LOG_LEVEL as 'none' | 'error' | 'warning' | 'info' | 'debug' || 'warning', + }); + + await client.start(); + sharedClient = client; + + logger.info('Copilot SDK client connected'); + return client; +} + +/** + * Build MCP server configs for the Copilot SDK session. + * + * Playwright MCP is added when the agent's prompt template has a mapping + * in MCP_AGENT_MAPPING. The shannon-helper tools are registered as native + * Copilot SDK tools instead (see buildShannonTools). + */ +function buildCopilotMcpServers( + _sourceDir: string, + agentName: string | null, + logger: ActivityLogger +): Record { + const servers: Record = {}; + + // Playwright MCP (when mapped) + if (agentName) { + const promptTemplate = AGENTS[agentName as AgentName].promptTemplate; + const playwrightMcpName = MCP_AGENT_MAPPING[promptTemplate as keyof typeof MCP_AGENT_MAPPING] || null; + + if (playwrightMcpName) { + logger.info(`Assigned ${agentName} -> ${playwrightMcpName} (Copilot mode)`); + + const userDataDir = `/tmp/${playwrightMcpName}`; + const isDocker = process.env.SHANNON_DOCKER === 'true'; + + const mcpArgs: string[] = [ + '@playwright/mcp@latest', + '--isolated', + '--user-data-dir', userDataDir, + ]; + + if (isDocker) { + mcpArgs.push('--executable-path', '/usr/bin/chromium-browser'); + mcpArgs.push('--browser', 'chromium'); + } + + const envVars: Record = { + PLAYWRIGHT_HEADLESS: 'true', + ...(isDocker ? { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' } : {}), + }; + + servers[playwrightMcpName] = { + command: 'npx', + args: mcpArgs, + tools: ['*'], + env: envVars, + } satisfies MCPLocalServerConfig; + } + } + + return servers; +} + +/** + * Build native Copilot SDK tools that replicate the shannon-helper MCP server. + * + * Registers save_deliverable as a native tool. This avoids needing a separate + * stdio MCP server process. + */ +function buildShannonTools(sourceDir: string): Tool[] { + const deliverableDir = path.join(sourceDir, 'deliverables'); + + return [ + { + name: 'save_deliverable', + description: 'Save a deliverable file (analysis reports, exploitation evidence, or vulnerability queues) to the deliverables directory.', + parameters: { + type: 'object', + properties: { + deliverable_type: { + type: 'string', + description: 'Type of deliverable to save (e.g., code_analysis, recon, injection_analysis, xss_analysis, auth_analysis, ssrf_analysis, authz_analysis, injection_exploitation, xss_exploitation, auth_exploitation, ssrf_exploitation, authz_exploitation, injection_queue, xss_queue, auth_queue, ssrf_queue, authz_queue, report)', + }, + content: { + type: 'string', + description: 'File content (markdown for analysis/evidence, JSON for queues)', + }, + file_path: { + type: 'string', + description: 'Path to a file whose contents should be used as the deliverable content. Use this instead of content for large reports.', + }, + }, + required: ['deliverable_type'], + }, + handler: async (args: { deliverable_type: string; content?: string; file_path?: string }) => { + try { + await fs.mkdirp(deliverableDir); + + // Resolve filename from deliverable type + const filenameMap: Record = { + code_analysis: 'code_analysis_deliverable.md', + recon: 'recon_deliverable.md', + injection_analysis: 'injection_analysis_deliverable.md', + xss_analysis: 'xss_analysis_deliverable.md', + auth_analysis: 'auth_analysis_deliverable.md', + ssrf_analysis: 'ssrf_analysis_deliverable.md', + authz_analysis: 'authz_analysis_deliverable.md', + injection_exploitation: 'injection_exploitation_evidence.md', + xss_exploitation: 'xss_exploitation_evidence.md', + auth_exploitation: 'auth_exploitation_evidence.md', + ssrf_exploitation: 'ssrf_exploitation_evidence.md', + authz_exploitation: 'authz_exploitation_evidence.md', + injection_queue: 'injection_queue.json', + xss_queue: 'xss_queue.json', + auth_queue: 'auth_queue.json', + ssrf_queue: 'ssrf_queue.json', + authz_queue: 'authz_queue.json', + report: 'comprehensive_security_assessment_report.md', + }; + + const filename = filenameMap[args.deliverable_type] || `${args.deliverable_type}.md`; + const filePath = path.join(deliverableDir, filename); + + // Resolve content + let content = args.content; + if (!content && args.file_path) { + const resolvedPath = path.isAbsolute(args.file_path) + ? args.file_path + : path.resolve(sourceDir, args.file_path); + + // Security: prevent path traversal + if (!resolvedPath.startsWith(path.resolve(sourceDir))) { + return `Error: Path "${args.file_path}" resolves outside allowed directory`; + } + + content = await fs.readFile(resolvedPath, 'utf-8'); + } + + if (!content) { + return 'Error: Either "content" or "file_path" must be provided'; + } + + await fs.writeFile(filePath, content, 'utf-8'); + return `Deliverable saved: ${filename}`; + } catch (error) { + return `Error saving deliverable: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }, + ]; +} + +/** + * Execute an agent prompt via the GitHub Copilot SDK. + * + * Drop-in replacement for runClaudePrompt — same signature, same return type. + */ +export async function runCopilotPrompt( + prompt: string, + sourceDir: string, + context: string = '', + description: string = 'Copilot analysis', + agentName: string | null = null, + _auditSession: AuditSession | null = null, + logger: ActivityLogger, + modelTier: ModelTier = 'medium' +): Promise { + const timer = new Timer(`copilot-${description.toLowerCase().replace(/\s+/g, '-')}`); + const fullPrompt = context ? `${context}\n\n${prompt}` : prompt; + + logger.info(`Running Copilot agent: ${description}...`); + + let turnCount = 0; + let result: string | null = null; + let totalCost = 0; + let model: string | undefined; + let apiErrorDetected = false; + + try { + // 1. Get or create the shared Copilot client + const client = await getOrCreateClient(logger); + + // 2. Resolve model for this tier + const resolvedModel = resolveCopilotModel(modelTier); + logger.info(`Copilot model: ${resolvedModel} (tier: ${modelTier})`); + + // 3. Build MCP servers and native tools + const mcpServers = buildCopilotMcpServers(sourceDir, agentName, logger); + const tools = buildShannonTools(sourceDir); + + // 4. Create a session + const sessionConfig: SessionConfig = { + model: resolvedModel, + workingDirectory: sourceDir, + onPermissionRequest: approveAll, + mcpServers, + tools, + systemMessage: { + mode: 'replace' as const, + content: 'You are a security testing agent. Follow the instructions in the user prompt precisely. Use the available tools to perform file operations, run commands, and interact with applications.', + }, + }; + + const session = await client.createSession(sessionConfig); + model = resolvedModel; + + // 5. Collect events for audit and result extraction + const assistantMessages: string[] = []; + let sessionError: string | undefined; + + const unsubscribe = session.on((event: SessionEvent) => { + switch (event.type) { + case 'assistant.turn_start': + turnCount++; + break; + case 'assistant.message': + if (event.data.content) { + assistantMessages.push(event.data.content); + } + break; + case 'assistant.usage': + if (event.data.cost) { + totalCost += event.data.cost; + } + if (event.data.model) { + model = event.data.model; + } + break; + case 'session.error': + sessionError = event.data.message; + logger.error(`Copilot session error: ${event.data.message}`); + break; + case 'tool.execution_complete': + if (!event.data.success && event.data.error) { + logger.warn(`Tool ${event.data.toolCallId} failed: ${event.data.error.message}`); + } + break; + } + }); + + // 6. Send prompt and wait for completion + // Use a generous timeout — pentest agents can run for 30+ minutes + const AGENT_TIMEOUT_MS = 45 * 60 * 1000; // 45 minutes + const response = await session.sendAndWait({ prompt: fullPrompt }, AGENT_TIMEOUT_MS); + + unsubscribe(); + + // 7. Extract final result + if (response?.data.content) { + result = response.data.content; + } else if (assistantMessages.length > 0) { + result = assistantMessages[assistantMessages.length - 1] ?? null; + } + + if (sessionError) { + apiErrorDetected = true; + } + + // 8. Clean up session + try { + await session.destroy(); + } catch { + // Best effort — session may already be closed + } + + // 9. Spending cap detection + if (isSpendingCapBehavior(turnCount, totalCost, result || '')) { + throw new PentestError( + `Spending cap likely reached (turns=${turnCount}, cost=$${totalCost}): ${(result || '').slice(0, 100)}`, + 'billing', + true + ); + } + + // 10. Return result + const duration = timer.stop(); + logger.info(`Copilot agent ${description} completed: ${turnCount} turns, $${totalCost.toFixed(4)}, ${duration}ms`); + + return { + result, + success: !!result, + duration, + turns: turnCount, + cost: totalCost, + model, + partialCost: totalCost, + apiErrorDetected, + }; + + } catch (error) { + const duration = timer.stop(); + const err = error as Error & { code?: string; status?: number }; + + logger.error(`Copilot agent ${description} failed: ${err.message}`); + + return { + error: err.message, + errorType: err.constructor.name, + prompt: fullPrompt.slice(0, 100) + '...', + success: false, + duration, + cost: totalCost, + retryable: isRetryableError(err), + }; + } +} + +/** Gracefully shut down the shared Copilot client. Called during worker shutdown. */ +export async function shutdownCopilotClient(): Promise { + if (sharedClient) { + try { + await sharedClient.stop(); + } catch { + try { await sharedClient.forceStop(); } catch { /* ignore */ } + } + sharedClient = null; + } +} diff --git a/src/ai/models.ts b/src/ai/models.ts index 7728215c..7687130e 100644 --- a/src/ai/models.ts +++ b/src/ai/models.ts @@ -24,6 +24,43 @@ const DEFAULT_MODELS: Readonly> = { large: 'claude-opus-4-6', }; +// Copilot model IDs — available through the GitHub Copilot API +const COPILOT_DEFAULT_MODELS: Readonly> = { + small: 'claude-haiku-4.5', + medium: 'claude-sonnet-4.6', + large: 'claude-opus-4.6', +}; + +/** All models available via the GitHub Copilot API. */ +export const COPILOT_AVAILABLE_MODELS: ReadonlyArray<{ name: string; id: string }> = [ + // Claude + { name: 'Claude Haiku 4.5', id: 'claude-haiku-4.5' }, + { name: 'Claude Sonnet 4', id: 'claude-sonnet-4' }, + { name: 'Claude Sonnet 4.5', id: 'claude-sonnet-4.5' }, + { name: 'Claude Sonnet 4.6', id: 'claude-sonnet-4.6' }, + { name: 'Claude Opus 4.1', id: 'claude-opus-41' }, + { name: 'Claude Opus 4.5', id: 'claude-opus-4.5' }, + { name: 'Claude Opus 4.6', id: 'claude-opus-4.6' }, + // GPT + { name: 'GPT-4o', id: 'gpt-4o' }, + { name: 'GPT-4.1', id: 'gpt-4.1' }, + { name: 'GPT-5', id: 'gpt-5' }, + { name: 'GPT-5 Mini', id: 'gpt-5-mini' }, + { name: 'GPT-5.1', id: 'gpt-5.1' }, + { name: 'GPT-5.1 Codex', id: 'gpt-5.1-codex' }, + { name: 'GPT-5.1 Codex Max', id: 'gpt-5.1-codex-max' }, + { name: 'GPT-5.1 Codex Mini', id: 'gpt-5.1-codex-mini' }, + { name: 'GPT-5.2', id: 'gpt-5.2' }, + { name: 'GPT-5.2 Codex', id: 'gpt-5.2-codex' }, + // Gemini + { name: 'Gemini 2.5 Pro', id: 'gemini-2.5-pro' }, + { name: 'Gemini 3 Flash', id: 'gemini-3-flash-preview' }, + { name: 'Gemini 3 Pro Preview', id: 'gemini-3-pro-preview' }, + { name: 'Gemini 3.1 Pro Preview', id: 'gemini-3.1-pro-preview' }, + // Other + { name: 'Grok Code Fast 1', id: 'grok-code-fast-1' }, +]; + /** Resolve a model tier to a concrete model ID. */ export function resolveModel(tier: ModelTier = 'medium'): string { switch (tier) { @@ -35,3 +72,16 @@ export function resolveModel(tier: ModelTier = 'medium'): string { return process.env.ANTHROPIC_MEDIUM_MODEL || DEFAULT_MODELS.medium; } } + +/** Resolve a model tier for the Copilot provider. */ +export function resolveCopilotModel(tier: ModelTier = 'medium'): string { + // User overrides take priority (same env vars) + switch (tier) { + case 'small': + return process.env.COPILOT_SMALL_MODEL || process.env.ANTHROPIC_SMALL_MODEL || COPILOT_DEFAULT_MODELS.small; + case 'large': + return process.env.COPILOT_LARGE_MODEL || process.env.ANTHROPIC_LARGE_MODEL || COPILOT_DEFAULT_MODELS.large; + default: + return process.env.COPILOT_MEDIUM_MODEL || process.env.ANTHROPIC_MEDIUM_MODEL || COPILOT_DEFAULT_MODELS.medium; + } +} diff --git a/src/services/preflight.ts b/src/services/preflight.ts index d050d054..fbafc5ef 100644 --- a/src/services/preflight.ts +++ b/src/services/preflight.ts @@ -25,6 +25,7 @@ import { ErrorCode } from '../types/errors.js'; import { type Result, ok, err } from '../types/result.js'; import { parseConfig } from '../config-parser.js'; import { resolveModel } from '../ai/models.js'; +import { isCopilotProvider } from '../ai/copilot-executor.js'; import type { ActivityLogger } from '../types/activity-logger.js'; // === Repository Validation === @@ -232,9 +233,53 @@ async function validateCredentials( // 4. Check that at least one credential is present if (!process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN) { + // 4a. GitHub Copilot mode — validate token via SDK + if (isCopilotProvider()) { + logger.info('Copilot provider detected — validating GitHub token...'); + const { CopilotClient } = await import('@github/copilot-sdk'); + const token = process.env.COPILOT_GITHUB_TOKEN + || process.env.GH_TOKEN + || process.env.GITHUB_TOKEN; + if (!token) { + return err( + new PentestError( + 'Copilot mode requires a GitHub token. Run "./shannon login" or set GITHUB_TOKEN in .env', + 'config', + false, + {}, + ErrorCode.AUTH_FAILED + ) + ); + } + try { + const testClient = new CopilotClient({ + githubToken: token, + useStdio: true, + autoStart: true, + autoRestart: false, + logLevel: 'none' as const, + }); + await testClient.start(); + const models = await testClient.listModels(); + await testClient.stop(); + logger.info(`Copilot token OK (${models.length} models available)`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return err( + new PentestError( + `GitHub token validation failed: ${message}. Run "./shannon login" to re-authenticate.`, + 'config', + false, + {}, + ErrorCode.AUTH_FAILED + ) + ); + } + return ok(undefined); + } return err( new PentestError( - 'No API credentials found. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env (or use CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock, or CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI)', + 'No API credentials found. Set ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, or GITHUB_TOKEN in .env (or use CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock, CLAUDE_CODE_USE_VERTEX=1 for Vertex AI, or GITHUB_TOKEN for Copilot)', 'config', false, {}, diff --git a/src/temporal/worker.ts b/src/temporal/worker.ts index b0f2f9bb..d12424dc 100644 --- a/src/temporal/worker.ts +++ b/src/temporal/worker.ts @@ -25,6 +25,7 @@ import { fileURLToPath } from 'node:url'; import path from 'node:path'; import dotenv from 'dotenv'; import * as activities from './activities.js'; +import { shutdownCopilotClient } from '../ai/copilot-executor.js'; dotenv.config(); @@ -67,6 +68,7 @@ async function runWorker(): Promise { try { await worker.run(); } finally { + await shutdownCopilotClient(); await connection.close(); console.log('Worker stopped'); }