diff --git a/.github/scripts/first-pass.mjs b/.github/scripts/first-pass.mjs new file mode 100644 index 00000000..b79cfa9c --- /dev/null +++ b/.github/scripts/first-pass.mjs @@ -0,0 +1,111 @@ +// Controlled first-pass reply for @ccu-bot — ONE model call, then post ONE +// comment. Deliberately NOT an autonomous agent: it uses no tools and never +// runs anything from the issue/PR, so a malicious public issue cannot steer it +// into misusing the token or leaking the key (prompt-injection safe). It only +// reads the project docs (+ PR diff), asks the model once, and comments. +// +// Env in: GH_TOKEN, REPO (owner/name), EVENT_KIND (issue|pr), ITEM_NUMBER, +// ITEM_TITLE, ITEM_BODY, DIFF_FILE (pr only), ANTHROPIC_API_KEY, +// ANTHROPIC_BASE_URL (third-party OK), CCU_BOT_MODEL. + +import { readFileSync, existsSync } from 'node:fs'; + +const env = process.env; +const base = (env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, ''); +const model = env.CCU_BOT_MODEL || 'deepseek-v4-flash'; +const isPr = env.EVENT_KIND === 'pr'; +const num = env.ITEM_NUMBER; + +const readDoc = (path, max = 12000) => { + try { + return existsSync(path) ? readFileSync(path, 'utf8').slice(0, max) : ''; + } catch { + return ''; + } +}; +const docs = + `# ARCHITECTURE.md\n${readDoc('ARCHITECTURE.md')}\n\n` + + `# CLAUDE.md\n${readDoc('CLAUDE.md')}\n\n` + + `# CONTRIBUTING.md\n${readDoc('CONTRIBUTING.md')}`; +let diff = ''; +if (isPr && env.DIFF_FILE) { + diff = readDoc(env.DIFF_FILE, 40000); +} + +const kind = isPr ? 'review' : 'reply'; +const system = [ + 'You are the ClaudeCodeUsage repository assistant, replying via Claude Code tooling.', + 'The underlying model may be a third-party Anthropic-format model, so do not claim to be Anthropic\'s Claude specifically.', + `Write ONE concise, concrete first-pass ${kind} for the ${isPr ? 'pull request' : 'issue'} below.`, + isPr + ? '- Cover correctness risks, convention/i18n/CHANGELOG gaps, and merge-readiness, grounded in the diff + docs.' + : '- Say what you understand the request to be, where in the architecture it relates, and a concrete suggested direction OR a specific clarifying question.', + `- Answer ONLY from the provided project docs${isPr ? ' and diff' : ''}. If they are insufficient, say exactly what is unclear and ask a specific question — NEVER guess or invent facts about the code.`, + '- Reply in the SAME language the author wrote in. Be specific and concise; no filler, no AI-flavoured padding.', + `- Start the comment with: "🤖 Automated first-pass ${kind} (via Claude Code)".`, + `- End with, on its own line: "_This is model-generated from the repository docs and the ${isPr ? 'PR diff' : 'issue'} — not a final decision. A maintainer reviews everything._"`, +].join('\n'); + +const userText = isPr + ? `PR #${num}: ${env.ITEM_TITLE}\n\n${env.ITEM_BODY || '(no description)'}\n\n--- DIFF (truncated) ---\n${diff}\n\n--- PROJECT DOCS ---\n${docs}` + : `Issue #${num}: ${env.ITEM_TITLE}\n\n${env.ITEM_BODY || '(no body)'}\n\n--- PROJECT DOCS ---\n${docs}`; + +const fail = (msg) => { + console.error(msg); + process.exit(1); +}; + +// 1) One model call (Anthropic Messages format; works against a third-party +// Anthropic-compatible endpoint via ANTHROPIC_BASE_URL). +let reply = ''; +try { + const res = await fetch(`${base}/v1/messages`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model, + max_tokens: 1200, + system, + messages: [{ role: 'user', content: userText }], + }), + }); + if (!res.ok) { + fail(`Model API error ${res.status}: ${(await res.text()).slice(0, 500)}`); + } + const data = await res.json(); + reply = (data.content || []) + .filter((b) => b && b.type === 'text' && typeof b.text === 'string') + .map((b) => b.text) + .join('') + .trim(); +} catch (e) { + fail(`Model call failed: ${e.message}`); +} +if (!reply) { + fail('Empty model reply.'); +} + +// 2) Post ONE comment via the GitHub API (issues + PRs share this endpoint). +const [owner, repo] = (env.REPO || '/').split('/'); +try { + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${num}/comments`, { + method: 'POST', + headers: { + authorization: `Bearer ${env.GH_TOKEN}`, + accept: 'application/vnd.github+json', + 'content-type': 'application/json', + 'user-agent': 'ccu-bot', + }, + body: JSON.stringify({ body: reply }), + }); + if (!res.ok) { + fail(`Comment post failed ${res.status}: ${(await res.text()).slice(0, 300)}`); + } +} catch (e) { + fail(`Comment post failed: ${e.message}`); +} +console.log(`Posted first-pass ${kind} on #${num}.`); diff --git a/.github/workflows/issue-first-pass.yml b/.github/workflows/issue-first-pass.yml index fe9773ba..1d0c4e77 100644 --- a/.github/workflows/issue-first-pass.yml +++ b/.github/workflows/issue-first-pass.yml @@ -1,16 +1,13 @@ -# Auto first-pass reply on every NEW issue (V2.2 automation — Carl's opt-in). +# Auto first-pass reply on every NEW issue — a CONTROLLED single model call +# (not an autonomous agent). See .github/scripts/first-pass.mjs: it reads the +# project docs, asks the model once, and posts one comment. No tools run, so a +# malicious public issue can't steer it into misusing the token or key. # -# When an issue is opened, the assistant reads the project's architecture docs -# and posts ONE helpful first-pass comment: what it understands, where in the -# code/architecture it points, and either a suggested direction or a clarifying -# question. Every comment is clearly labelled automated and ends with a -# disclaimer that a maintainer follows up. -# -# SAFE BY DEFAULT — inert until the owner sets: -# 1. Repo variable AUTOMATION_ENABLED = 'true' (kill switch) -# 2. Repo secret ANTHROPIC_API_KEY (official or third-party key) -# Optional: repo variable ANTHROPIC_BASE_URL for a third-party Anthropic-format -# endpoint. Bot-authored issues are skipped (no loops). +# INERT until the owner sets: +# AUTOMATION_ENABLED = 'true' (repo variable, kill switch) +# ANTHROPIC_API_KEY (repo secret — official or third-party key) +# Optional repo variables: ANTHROPIC_BASE_URL (third-party Anthropic-format +# endpoint), CCU_BOT_MODEL (defaults to deepseek-v4-flash). name: Issue first-pass (auto) on: @@ -18,9 +15,8 @@ on: types: [opened] permissions: - contents: read # read repo files to answer accurately (never guess) - issues: write # post the reply - id-token: write # claude-code-action mints its GitHub token via OIDC + contents: read # read the repo docs the reply is grounded in + issues: write # post the comment concurrency: group: issue-first-pass-${{ github.event.issue.number }} @@ -30,7 +26,7 @@ jobs: reply: if: vars.AUTOMATION_ENABLED == 'true' && github.event.issue.user.type != 'Bot' runs-on: ubuntu-latest - timeout-minutes: 12 + timeout-minutes: 8 steps: - name: Checkout repository uses: actions/checkout@v6 @@ -38,35 +34,14 @@ jobs: fetch-depth: 1 - name: First-pass reply - uses: anthropics/claude-code-action@v1 env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + EVENT_KIND: issue + ITEM_NUMBER: ${{ github.event.issue.number }} + ITEM_TITLE: ${{ github.event.issue.title }} + ITEM_BODY: ${{ github.event.issue.body }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }} - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # Use the workflow token so no Claude GitHub App install is needed - # (we drive a third-party model; the app is only Anthropic's GitHub auth). - github_token: ${{ github.token }} - claude_args: "--max-turns 15" - prompt: | - You are the ClaudeCodeUsage repository assistant, running via Claude Code - (the model may be a third-party Anthropic-format model, so do not claim to - be Anthropic's Claude specifically). - - A new issue was just opened (title/body in the event context). Post ONE - comment as a first-pass response. Rules: - - First read CLAUDE.md and CONTRIBUTING.md (and ARCHITECTURE.md if present) - for context. - - If those docs are NOT enough to answer accurately, READ the relevant - repository files to find the real answer. Never guess or invent an - answer — if you still can't determine it, say what's unclear and ask a - specific clarifying question instead of speculating. - - Be concrete and concise: what you understand the request to be, where in - the code/architecture it relates, and a suggested direction OR the - clarifying question needed. No filler, no AI-flavoured padding. - - Reply in the same language the issue author used. - - START the comment with: "🤖 Automated first-pass reply (via Claude Code)". - - END the comment with this disclaimer on its own line: - "_This is model-generated from the repository's architecture docs and - the issue description — not a final decision. A maintainer will review - and follow up._" - - Do not change labels, close the issue, or open PRs. Just the one comment. + CCU_BOT_MODEL: ${{ vars.CCU_BOT_MODEL }} + run: node .github/scripts/first-pass.mjs diff --git a/.github/workflows/pr-first-pass.yml b/.github/workflows/pr-first-pass.yml index 4b1e0e45..601250e2 100644 --- a/.github/workflows/pr-first-pass.yml +++ b/.github/workflows/pr-first-pass.yml @@ -1,12 +1,10 @@ -# Auto first-pass review on every NEW pull request (V2.2 automation — opt-in). +# Auto first-pass review on every NEW pull request — a CONTROLLED single model +# call (not an autonomous agent). Uses pull_request_target for the token but +# NEVER runs the PR's code: it checks out the BASE repo, fetches the diff via the +# API, asks the model once, and posts one comment. No tools → injection-safe. # -# Uses pull_request_target so it has the token to comment, but it must NEVER run -# the PR's code with that token. We check out the BASE repo (the default for -# pull_request_target), read the PR diff via the API, and post ONE first-pass -# review comment. Fork code is never executed here. -# -# SAFE BY DEFAULT — inert until the owner sets AUTOMATION_ENABLED='true' and the -# ANTHROPIC_API_KEY secret (official or third-party). Bot-authored PRs skipped. +# INERT until AUTOMATION_ENABLED = 'true' + the ANTHROPIC_API_KEY secret. +# Optional repo vars: ANTHROPIC_BASE_URL, CCU_BOT_MODEL (default deepseek-v4-flash). name: PR first-pass (auto) on: @@ -16,7 +14,6 @@ on: permissions: contents: read pull-requests: write - id-token: write # claude-code-action mints its GitHub token via OIDC concurrency: group: pr-first-pass-${{ github.event.pull_request.number }} @@ -24,47 +21,31 @@ concurrency: jobs: review: - # Skip bots and huge diffs (a > ~3000-line PR wants a human first). if: vars.AUTOMATION_ENABLED == 'true' && github.event.pull_request.user.type != 'Bot' runs-on: ubuntu-latest - timeout-minutes: 12 + timeout-minutes: 8 steps: - # Base repo only — do NOT check out or run the PR head with the token. + # Base repo only — never checks out or runs the PR head with the token. - name: Checkout base repository uses: actions/checkout@v6 with: fetch-depth: 1 + - name: Fetch PR diff (via API, no code run) + env: + GH_TOKEN: ${{ github.token }} + run: gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} > /tmp/pr.diff || true + - name: First-pass review - uses: anthropics/claude-code-action@v1 env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + EVENT_KIND: pr + ITEM_NUMBER: ${{ github.event.pull_request.number }} + ITEM_TITLE: ${{ github.event.pull_request.title }} + ITEM_BODY: ${{ github.event.pull_request.body }} + DIFF_FILE: /tmp/pr.diff + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }} - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # Use the workflow token so no Claude GitHub App install is needed - # (we drive a third-party model; the app is only Anthropic's GitHub auth). - github_token: ${{ github.token }} - claude_args: "--max-turns 15" - prompt: | - You are the ClaudeCodeUsage repository assistant, running via Claude Code - (the model may be a third-party Anthropic-format model; do not claim to be - Anthropic's Claude specifically). - - A new pull request was opened. Do a first-pass review and post ONE comment. - SECURITY: never execute the PR's code. Read the PR DIFF via the GitHub API - and the repository's own files only. - Rules: - - First read CLAUDE.md and CONTRIBUTING.md (and ARCHITECTURE.md if present) - for the - conventions (compile-clean, node:test green, 7-language i18n, opt-in - defaults, CHANGELOG). If those aren't enough to judge accurately, read - the relevant base-repo files — never guess. - - Cover: correctness risks, convention/i18n/CHANGELOG gaps, and - merge-readiness. Be specific and concise; skip praise padding. - - If the diff is very large (> ~3000 lines) or you're unsure, say a human - should review rather than speculate. - - START with: "🤖 Automated first-pass review (via Claude Code)". - - END with: "_This is model-generated from the architecture docs and the - PR diff — not a merge decision. A maintainer reviews everything._" - - Do not approve, request changes as a formal review, merge, or push. One - comment only. + CCU_BOT_MODEL: ${{ vars.CCU_BOT_MODEL }} + run: node .github/scripts/first-pass.mjs