From 301ef9c4b5aedaa88d4542050fb53a01986420a1 Mon Sep 17 00:00:00 2001 From: Izzy Weinberg Date: Wed, 2 Sep 2026 12:36:05 +0300 Subject: [PATCH 1/3] feat(ci): install the canonical @fuze entrypoint; rename claude-auto-pr -> fuze-auto-pr Adds .github/workflows/fuze.yml, the canonical @fuze handler from FuzeSDLC. Until now @fuze was answered in exactly one repo (FuzeInfra) while this repo's issue templates already say "@fuze -- please triage", so the mention landed nowhere and looked indistinguishable from an agent that had not replied yet. No cluster credential of any kind: the privileged, cluster-capable variant is a separate template installed only where the manifest declares clusterOps.enabled, which only FuzeInfra does. Verified on this repo -- zero privileged identifiers in the rendered entrypoint. Also renames claude-auto-pr.yml -> fuze-auto-pr.yml (naming only; it makes no LLM call). The old file is removed rather than left beside the new one, which would double-fire on a push to claude/**. The trigger now covers both claude/** and fuze/**; no existing prefix was dropped. Co-Authored-By: Claude Opus 5 --- .fuze/installed.json | 38 ++- .../{claude-auto-pr.yml => fuze-auto-pr.yml} | 26 +- .github/workflows/fuze.yml | 293 ++++++++++++++++++ 3 files changed, 338 insertions(+), 19 deletions(-) rename .github/workflows/{claude-auto-pr.yml => fuze-auto-pr.yml} (67%) create mode 100644 .github/workflows/fuze.yml diff --git a/.fuze/installed.json b/.fuze/installed.json index 906ea84..be6ff08 100644 --- a/.fuze/installed.json +++ b/.fuze/installed.json @@ -480,18 +480,6 @@ "repoName": "izzywdev/FuzeKeys" } }, - ".github/workflows/claude-auto-pr.yml": { - "capability": "workflows", - "mode": "stamped", - "sourcePath": "workflow-templates/claude-auto-pr.yml", - "sourceSha256": "c8baa726dbaf59f4f4abb6f1e093d86635be1dc6e4465a4a2e1c41265046835e", - "renderedSha256": "c8baa726dbaf59f4f4abb6f1e093d86635be1dc6e4465a4a2e1c41265046835e", - "renderVars": { - "defaultBranch": "master", - "baselineRef": "main", - "repoName": "izzywdev/FuzeKeys" - } - }, ".github/workflows/claude.yml": { "capability": "workflows", "mode": "stamped", @@ -528,6 +516,18 @@ "repoName": "izzywdev/FuzeKeys" } }, + ".github/workflows/fuze-auto-pr.yml": { + "capability": "workflows", + "mode": "stamped", + "sourcePath": "workflow-templates/fuze-auto-pr.yml", + "sourceSha256": "38b0c76d7f488d2525ce4cddddfedf2d93e48507013d5acddf6126b674f4cebc", + "renderedSha256": "38b0c76d7f488d2525ce4cddddfedf2d93e48507013d5acddf6126b674f4cebc", + "renderVars": { + "defaultBranch": "master", + "baselineRef": "main", + "repoName": "izzywdev/FuzeKeys" + } + }, ".github/workflows/fuze-ci-autofix.yml": { "capability": "workflows", "mode": "stamped", @@ -552,6 +552,18 @@ "repoName": "izzywdev/FuzeKeys" } }, + ".github/workflows/fuze.yml": { + "capability": "workflows", + "mode": "stamped", + "sourcePath": "workflow-templates/fuze.yml", + "sourceSha256": "1c80ac7e263fce73e866bebf98bde486f5950e79c0819f48ce62b8828487bb1b", + "renderedSha256": "1c80ac7e263fce73e866bebf98bde486f5950e79c0819f48ce62b8828487bb1b", + "renderVars": { + "defaultBranch": "master", + "baselineRef": "main", + "repoName": "izzywdev/FuzeKeys" + } + }, ".github/workflows/gate-frames-stamped.yml": { "capability": "workflows", "mode": "stamped", @@ -1146,4 +1158,4 @@ "renderedSha256": "26f87785b713723dd2f3e2264dc506aa00d5386c5829960a7828385c5ebff66b" } } -} \ No newline at end of file +} diff --git a/.github/workflows/claude-auto-pr.yml b/.github/workflows/fuze-auto-pr.yml similarity index 67% rename from .github/workflows/claude-auto-pr.yml rename to .github/workflows/fuze-auto-pr.yml index fcb6f45..2aa93bc 100644 --- a/.github/workflows/claude-auto-pr.yml +++ b/.github/workflows/fuze-auto-pr.yml @@ -1,14 +1,28 @@ -# fuze:managed template=claude-auto-pr.yml baseline=main digest=sha256:723395e4fc0a180f3dab3e95a1db50c10e49881088c27f3e63e53c030947060e -name: Auto-PR from claude branches +# fuze:managed template=fuze-auto-pr.yml baseline=main digest=sha256:81f87278191d3589313218d5737228e94cd6901906299747a81c6853680f5e45 +name: Auto-PR from agent branches -# Closes the issue->PR autonomy gap: `anthropics/claude-code-action` (in claude.yml) -# pushes a `claude/**` branch and only posts a "Create PR" link — it does not open -# the PR itself. This workflow opens a DRAFT PR from any pushed `claude/**` branch -# when one doesn't already exist (draft = a human still reviews before merge). +# RENAMED from `claude-auto-pr.yml` (and `name:` from "Auto-PR from claude branches") for +# naming consistency with the `@fuze` entrypoint (`fuze.yml`). NAMING ONLY — this workflow +# makes NO LLM call of any kind: it is `gh pr create` and nothing else, so there is nothing +# here to route through the LiteLLM gateway and nothing was added. Anyone reading the rename +# as "this now goes through fuze-code-action" is reading it wrong. +# +# Closes the issue->PR autonomy gap: `anthropics/claude-code-action` (in `claude.yml` and +# `fuze.yml`) pushes a branch and only posts a "Create PR" link — it does not open the PR +# itself. This workflow opens a DRAFT PR from any pushed agent branch when one doesn't +# already exist (draft = a human still reviews before merge). +# +# BOTH PREFIXES, deliberately. `claude/**` is what claude-code-action actually names its +# branches today and MUST stay — dropping it would silently stop every agent PR in the +# fleet. `fuze/**` is added because after the rename the filename promises fuze branches, +# and a repo whose `@fuze` handler pushes that prefix (FuzeInfra's cluster-capable variant +# does) would otherwise be covered by the name and not by the trigger. Strictly additive: +# nothing that worked before stops working. on: push: branches: - 'claude/**' + - 'fuze/**' permissions: contents: read diff --git a/.github/workflows/fuze.yml b/.github/workflows/fuze.yml new file mode 100644 index 0000000..0c07b9c --- /dev/null +++ b/.github/workflows/fuze.yml @@ -0,0 +1,293 @@ +# fuze:managed template=fuze.yml baseline=main digest=sha256:687aa20600c85f6a233c3289ff230de51fa6e6a08a3dbfe558bc07a9c764c24d +name: Fuze + +# THE CANONICAL `@fuze` ENTRYPOINT. Every onboarded repo gets this file, and this is the +# handler a human — or an automated workflow that posts a comment — reaches by mentioning +# `@fuze` on an issue, an issue comment, a PR review or a review comment. +# +# WHY THIS EXISTS AS A CANONICAL TEMPLATE AT ALL. `@fuze` was a real, working entrypoint in +# exactly ONE repo (izzywdev/FuzeInfra) and in no other, so `@fuze` in any other repo was a +# mention nothing answered. Community issue templates already seed `@fuze — please triage.` +# (community-templates/.github/ISSUE_TEMPLATE/*.md), and the baseline documents `@fuze` as +# the delegation handle, so 21 of 22 repos were pointed at a handler they did not have. +# +# WHAT WAS **NOT** COPIED HERE, AND WHY THAT IS THE WHOLE POINT. +# FuzeInfra's version of this file is CLUSTER-CAPABLE: it mounts the prod k3s kubeconfig +# secret, installs the cluster and IaC CLIs, and runs on FuzeInfra's in-cluster ARC pool so +# it can reach a ClusterIP-only gateway. Promoting THAT verbatim would have handed prod +# cluster credentials to every product repo in the family — a privilege escalation dressed +# as a sync — and would have pinned every repo's `@fuze` to a self-hosted pool only +# FuzeInfra has, where a job does not fail but QUEUES FOREVER (governance/ci-runners.md). +# So the capability is SPLIT: +# * THIS file — the baseline entrypoint, no cluster credential of any kind, on the repo's +# own runner. It is the only `@fuze` handler a product repo ever gets. +# * `workflow-templates/fuze-cluster.yml` — the privileged variant, installed ONLY into a +# repo whose `.fuze/manifest.json` declares `clusterOps.enabled: true` (today: FuzeInfra +# alone, which owns the cluster), and inert even there until `vars.FUZE_CLUSTER_ENTRYPOINT` +# is set. See that file's header and scripts/bootstrap/caps/workflows.py CONDITIONAL. +# +# The privileged identifiers — the kubeconfig secret name, the IaC CLI name, and the +# self-hosted pool label — are deliberately ABSENT FROM THIS FILE AS LITERAL TEXT, not merely +# unused. Grep the three of them across a non-opted-in repo's `.github/workflows/` and the +# `@fuze` entrypoint contributes zero hits; that is the check. Do not reintroduce those +# strings here, not even inside a comment: to an auditor's grep a prose mention is +# indistinguishable from a real reference, which is exactly how a checkable security property +# decays into an unverifiable claim. The strings live in `fuze-cluster.yml` and nowhere else. +# +# LLM CREDENTIAL — routed, never hardcoded. FuzeInfra's version sets +# `ANTHROPIC_BASE_URL: http://litellm.fuzeinfra.svc.cluster.local:4000` directly. That is +# correct for a pod inside the cluster and USELESS anywhere else: the name does not resolve +# from a GitHub-hosted runner, so a verbatim copy would have failed DNS on 21 repos. This +# template routes through `./.github/actions/fuze-code-action`, which resolves the endpoint +# via `./.github/actions/llm-endpoint` — a bounded-timeout probe of the in-cluster gateway +# that falls back to a direct vendor key when the gateway is unreachable. That fallback is +# precisely what the FuzeInfra file lacks. +# +# HONEST LIMIT OF THAT ROUTING, stated here so nobody reads more resilience into it than +# exists: with no `task-prompt` this is MENTION MODE, and fuze-code-action DELIBERATELY +# disables rungs 2 and 3 there (codex-action and run-gemini-cli have no GitHub event +# context, so a rung that "succeeds" at seeing nothing is a manufactured green). The +# openai/gemini keys are passed below for shape-parity with the other call sites and are +# genuinely inert on this path. In mention mode there is exactly ONE rung, and if the +# Anthropic-or-LiteLLM leg has no working credential, this workflow fails — it does not +# quietly answer from somewhere else. +# +# RELATIONSHIP TO `claude.yml`. `@claude` stays as an explicit, human-typed escape hatch to +# the same routed path. `@fuze` is the family's DEFAULT handle and the one every doc, agent +# and skill now names (governance/routing.md). Both files gate independently, so a comment +# containing both mentions runs both handlers — write one or the other, not both. +# +# SECURITY — trusted authors only. Gate 2 restricts triggering to OWNER/MEMBER/COLLABORATOR. +# This handler holds a repo-write token and an agent that will push branches with it; an +# untrusted commenter must never be able to drive that. The gate is kept here even though +# this variant holds no cluster credential, because "no cluster credential" is not the same +# as "harmless". +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned, labeled] + pull_request_review: + types: [submitted] + +jobs: + fuze: + # DEDUP: at most one run per issue/PR at a time (latest wins). cancel-in-progress + # collapses near-simultaneous duplicates — an issue opened WITH labels emits both + # `issues.opened` and `issues.labeled`, and without this both would start an agent. + concurrency: + group: fuze-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: true + # Gate 1: `@fuze` is actually mentioned (or the issue is labeled/assigned `fuze`). + # Gate 2 (SECURITY): trusted collaborator — see the header. + # Gate 3 (LOOP-BREAKER): the `fuze-loop-guard` sentinel. The failure notice this + # workflow posts below itself contains `@fuze`, and would otherwise re-trigger the + # workflow forever. DO NOT remove the sentinel from that comment body. + # Gate 4 (PRIVILEGE SPLIT): stand down in a repo that has activated the cluster-capable + # entrypoint, so `fuze.yml` and `fuze-cluster.yml` can never both answer the same + # mention. Unset/absent — every repo except an activated FuzeInfra — means THIS + # handler runs, which is the fail-safe direction: the default is the UNPRIVILEGED one. + if: | + vars.FUZE_CLUSTER_ENTRYPOINT != 'true' && + ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@fuze') && + !contains(github.event.comment.body, 'fuze-loop-guard') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@fuze') && + !contains(github.event.comment.body, 'fuze-loop-guard') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@fuze') && + !contains(github.event.review.body, 'fuze-loop-guard') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && github.event.action == 'opened' && + (contains(github.event.issue.body, '@fuze') || contains(github.event.issue.title, '@fuze')) && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)) || + (github.event_name == 'issues' && github.event.action == 'labeled' && + github.event.label.name == 'fuze' && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)) || + (github.event_name == 'issues' && github.event.action == 'assigned' && + github.event.assignee.login == 'fuze' && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)) + ) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + + - name: Checkout PR branch (when triggered from a PR) + if: | + (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || + github.event_name == 'pull_request_review_comment' || + github.event_name == 'pull_request_review' + # Every `github` context value reaches the shell through env:, never through + # `${{ }}` inside run:. A branch name is attacker-controlled and git permits `;`, + # `|`, `$`, `"` and backticks in a ref, so an interpolated head.ref executes on the + # runner with the job's token in scope — the documented GitHub Actions script + # injection. actionlint flags it as `expression`, semgrep as run-shell-injection. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + REPO: ${{ github.repository }} + PR_NUM: ${{ github.event.issue.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + if [ "$EVENT_NAME" = "issue_comment" ]; then + HEAD_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUM}" --jq .head.sha) + HEAD_REF=$(gh api "repos/${REPO}/pulls/${PR_NUM}" --jq .head.ref) + else + HEAD_SHA="$PR_HEAD_SHA" + HEAD_REF="$PR_HEAD_REF" + fi + git fetch origin "$HEAD_SHA" + git checkout "$HEAD_SHA" + echo "Checked out PR branch: $HEAD_REF @ $HEAD_SHA" + + - name: Run Fuze + id: fuze + # continue-on-error so the detect + alert steps below run and OWN the job's final + # status. Without it the action fails the job outright and the requester is never + # told on their own thread — the failure is only visible to whoever opens the run + # log, which in practice is nobody. + continue-on-error: true + uses: ./.github/actions/fuze-code-action + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + litellm-key: ${{ secrets.LITELLM_FUZE_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + # Inert on this path — see MENTION MODE in the header. Passed for shape-parity + # with the fleet's other fuze-code-action call sites, NOT as a claimed fallback. + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + gemini-api-key: ${{ secrets.GEMINI_API_KEY }} + show-full-output: true + # allowed_bots — REQUIRED here, and not for the reason it is required in + # governance-nightly.yml. That one is a cron, whose actor is the scheduler + # identity. This one is mention-triggered, and a mention can legitimately be + # posted BY a workflow: the family's CI-failure triage, Argo out-of-sync autofix, + # nightly-integration and crit-alert workflows all comment `@fuze` to hand work + # over. The actor on that `issue_comment` event is the bot that posted it, and + # claude-code-action's human-actor check aborts the rung ~1s in, BEFORE any model + # call, with "Workflow initiated by non-human actor: . Add bot to + # allowed_bots list or use '*' to allow all bots." — a failure that looks in a + # skimmed log exactly like a provider outage. + # + # Deliberately a NAMED list, never `*`. Under `*` any bot-authored event — every + # repo here runs dependabot — could start an agent run holding the resolved LLM + # credential and repo write. `fuzeone-bot` is the fleet's scheduler/automation + # identity; `github-actions` covers a comment posted by a workflow using the + # default token; `fuze-agent` is the App identity agent PRs are opened under + # (governance/identity-and-attribution.md). Gate 2 above still independently + # requires the commenter to be OWNER/MEMBER/COLLABORATOR, so this list widens + # only the actor-type check, never the trust check. + allowed-bots: "fuzeone-bot,github-actions,fuze-agent" + # Without these, every mutating tool call (git push, gh pr/issue create, file + # edits) sits behind an interactive approval gate that nothing can clear in + # headless CI and is silently DENIED — the agent reads the repo, reports success, + # and changes nothing. Measured across the fleet on exactly this defect + # (governance-nightly.yml's header records the numbers). + additional-permissions: | + Bash + Write + Edit + MultiEdit + Task + TodoWrite + claude-args: >- + --permission-mode bypassPermissions + --max-turns 60 + --append-system-prompt "EXPERT-FIRST: begin every task by invoking this repo's -expert subagent (via Task) to load its architecture/deploy/gotcha context before planning or coding; the expert is named in .fuze/manifest.json under `expert`. Follow governance/routing.md for ownership: refuse work that belongs to another agent and name the correct owner. This runner holds NO cluster credential and NO prod access — never attempt cluster or infrastructure-as-code commands here (they are not installed and there is no credential for them); infra changes are delegated to FuzeInfra via a `@fuze` issue (CLAUDE.baseline.md 1), and cluster READS are self-service via FuzeInfra's cluster-query workflow. End issue replies with DONE: or BLOCKED: so cross-repo monitors can key off it. CROSS-REPO HAND-OFF: if a delegated issue's REMAINING work belongs to another repo, do NOT leave it open here — open (or comment on) an issue in that repo mentioning its `@fuze` with full context and a STATE block, post a closing note here, and CLOSE this issue." + + # DETECT: did the responder actually fail? Two signals — the composite action's own + # step outcome, and `is_error` in claude-code-action's execution log (a run can end + # is_error WITHOUT failing the step, which is how a dead responder reported green). + - name: Detect Fuze failure + id: detect + if: always() + env: + STEP_OUTCOME: ${{ steps.fuze.outcome }} + MODE: ${{ steps.fuze.outputs.mode }} + CONCLUSION: ${{ steps.fuze.outputs.conclusion }} + run: | + set -uo pipefail + failed=false; reason="" + if [ "${STEP_OUTCOME:-}" = "failure" ]; then + failed=true + reason="fuze-code-action failed (mode=${MODE:-?}, conclusion=${CONCLUSION:-?}). In mention mode there is only one rung, so this is the Anthropic-or-LiteLLM leg failing — check LITELLM_FUZE_KEY and ANTHROPIC_API_KEY on this repo." + elif [ "${CONCLUSION:-}" = "failure" ]; then + failed=true + reason="the responder ran and reported conclusion=failure (mode=${MODE:-?}) — it errored before completing the task" + fi + echo "failed=$failed" >> "$GITHUB_OUTPUT" + { + echo "reason<> "$GITHUB_OUTPUT" + if [ "$failed" = "true" ]; then + echo "::error::@fuze responder failed: $reason" + else + echo "@fuze responder completed without an error signal (mode=${MODE:-?})" + fi + + # ALERT: a failed responder tells the requester on their own thread, and pings + # Telegram where that is configured. Without this the requester waits on a reply that + # is never coming, which is indistinguishable from the agent still thinking. + - name: Alert on Fuze failure + if: always() && steps.detect.outputs.failed == 'true' + env: + TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + ISSUE: ${{ github.event.issue.number || github.event.pull_request.number }} + ACTOR: ${{ github.actor }} + REASON: ${{ steps.detect.outputs.reason }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -uo pipefail + if [ -n "${TOKEN:-}" ]; then + TEXT=$(printf '@fuze responder FAILED\nRepo: %s\nIssue/PR: #%s\nBy: %s\nReason: %s\nRun: %s' \ + "$REPO" "${ISSUE:-?}" "$ACTOR" "$REASON" "$RUN_URL") + curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \ + --data-urlencode "chat_id=${CHAT:-}" \ + --data-urlencode "text=${TEXT}" \ + --data "disable_web_page_preview=true" >/dev/null \ + && echo "telegram alert sent" || echo "::warning::telegram send failed" + else + echo "::notice::TELEGRAM_BOT_TOKEN unset — skipping Telegram alert" + fi + # LOOP-BREAKER: this comment mentions `@fuze` and would otherwise re-trigger this + # workflow. The hidden `fuze-loop-guard` sentinel is what Gate 3 keys off to + # ignore it — DO NOT remove it. + if [ -n "${ISSUE:-}" ]; then + { + printf '\n' + printf '### Fuze responder did not run to completion\n\n' + printf 'The responder errored before finishing your request, so **no changes were made and no branch was created**.\n\n' + printf -- '- **Reason:** %s\n' "$REASON" + printf -- '- **Run log:** %s\n\n' "$RUN_URL" + printf 'This notice is automated and will **not** retry on its own. Re-trigger by posting a new comment mentioning `@fuze` once the cause is fixed.\n' + } > "${RUNNER_TEMP}/fuze-fail.md" + gh issue comment "$ISSUE" --repo "$REPO" --body-file "${RUNNER_TEMP}/fuze-fail.md" \ + && echo "posted failure notice to #$ISSUE" || echo "::warning::could not comment on #$ISSUE" + fi + + # Reflect the failure in the job status AFTER alerting had a chance to run. + - name: Fail job if Fuze failed + if: always() && steps.detect.outputs.failed == 'true' + env: + REASON: ${{ steps.detect.outputs.reason }} + run: | + echo "::error::Marking job failed — @fuze responder did not complete: ${REASON}" + exit 1 From fc955d56291e41f2f5cec2a15421c1ff3ca5971a Mon Sep 17 00:00:00 2001 From: Izzy Weinberg Date: Wed, 2 Sep 2026 12:45:37 +0300 Subject: [PATCH 2/3] feat(ci): install the canonical @fuze entrypoint; rename claude-auto-pr -> fuze-auto-pr Adds .github/workflows/fuze.yml, the canonical @fuze handler from FuzeSDLC. Until now @fuze was answered in exactly one repo (FuzeInfra) while this repo's issue templates already say "@fuze -- please triage", so the mention landed nowhere and looked indistinguishable from an agent that had not replied yet. No cluster credential of any kind: the privileged, cluster-capable variant is a separate template installed only where the manifest declares clusterOps.enabled, which only FuzeInfra does. Verified on this repo -- zero privileged identifiers in the rendered entrypoint. Also renames claude-auto-pr.yml -> fuze-auto-pr.yml (naming only; it makes no LLM call). The old file is removed rather than left beside the new one, which would double-fire on a push to claude/**. The trigger now covers both claude/** and fuze/**; no existing prefix was dropped. Co-Authored-By: Claude Opus 5 --- .fuze/installed.json | 4 ++-- .github/workflows/fuze.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.fuze/installed.json b/.fuze/installed.json index be6ff08..46dd5d2 100644 --- a/.fuze/installed.json +++ b/.fuze/installed.json @@ -556,8 +556,8 @@ "capability": "workflows", "mode": "stamped", "sourcePath": "workflow-templates/fuze.yml", - "sourceSha256": "1c80ac7e263fce73e866bebf98bde486f5950e79c0819f48ce62b8828487bb1b", - "renderedSha256": "1c80ac7e263fce73e866bebf98bde486f5950e79c0819f48ce62b8828487bb1b", + "sourceSha256": "10fbd42c1d3d816f890a7270b9ebdedbbd698921da956e10b3dbc8be90f2c683", + "renderedSha256": "10fbd42c1d3d816f890a7270b9ebdedbbd698921da956e10b3dbc8be90f2c683", "renderVars": { "defaultBranch": "master", "baselineRef": "main", diff --git a/.github/workflows/fuze.yml b/.github/workflows/fuze.yml index 0c07b9c..c0d53ed 100644 --- a/.github/workflows/fuze.yml +++ b/.github/workflows/fuze.yml @@ -1,4 +1,4 @@ -# fuze:managed template=fuze.yml baseline=main digest=sha256:687aa20600c85f6a233c3289ff230de51fa6e6a08a3dbfe558bc07a9c764c24d +# fuze:managed template=fuze.yml baseline=main digest=sha256:6d63051eaf4c60a12997915510944b17d6fe44616b18e602950f0eb5868bf556 name: Fuze # THE CANONICAL `@fuze` ENTRYPOINT. Every onboarded repo gets this file, and this is the @@ -277,7 +277,7 @@ jobs: printf 'The responder errored before finishing your request, so **no changes were made and no branch was created**.\n\n' printf -- '- **Reason:** %s\n' "$REASON" printf -- '- **Run log:** %s\n\n' "$RUN_URL" - printf 'This notice is automated and will **not** retry on its own. Re-trigger by posting a new comment mentioning `@fuze` once the cause is fixed.\n' + printf 'This notice is automated and will **not** retry on its own. Re-trigger by posting a new comment mentioning @fuze once the cause is fixed.\n' } > "${RUNNER_TEMP}/fuze-fail.md" gh issue comment "$ISSUE" --repo "$REPO" --body-file "${RUNNER_TEMP}/fuze-fail.md" \ && echo "posted failure notice to #$ISSUE" || echo "::warning::could not comment on #$ISSUE" From 648acadb55193b5b0d7932bb322eb38987c2234f Mon Sep 17 00:00:00 2001 From: Izzy Weinberg Date: Wed, 2 Sep 2026 12:56:35 +0300 Subject: [PATCH 3/3] feat(ci): install the canonical @fuze entrypoint; rename claude-auto-pr -> fuze-auto-pr Adds .github/workflows/fuze.yml, the canonical @fuze handler from FuzeSDLC. Until now @fuze was answered in exactly one repo (FuzeInfra) while this repo's issue templates already say "@fuze -- please triage", so the mention landed nowhere and looked indistinguishable from an agent that had not replied yet. No cluster credential of any kind: the privileged, cluster-capable variant is a separate template installed only where the manifest declares clusterOps.enabled, which only FuzeInfra does. Verified on this repo -- zero privileged identifiers in the rendered entrypoint. Also renames claude-auto-pr.yml -> fuze-auto-pr.yml (naming only; it makes no LLM call). The old file is removed rather than left beside the new one, which would double-fire on a push to claude/**. The trigger now covers both claude/** and fuze/**; no existing prefix was dropped. Co-Authored-By: Claude Opus 5 --- .fuze/installed.json | 4 ++-- .github/workflows/fuze.yml | 48 ++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/.fuze/installed.json b/.fuze/installed.json index 46dd5d2..369d812 100644 --- a/.fuze/installed.json +++ b/.fuze/installed.json @@ -556,8 +556,8 @@ "capability": "workflows", "mode": "stamped", "sourcePath": "workflow-templates/fuze.yml", - "sourceSha256": "10fbd42c1d3d816f890a7270b9ebdedbbd698921da956e10b3dbc8be90f2c683", - "renderedSha256": "10fbd42c1d3d816f890a7270b9ebdedbbd698921da956e10b3dbc8be90f2c683", + "sourceSha256": "606fed71128968519719435844590c65152081ea6e18a1e6eb4f783de846fdc5", + "renderedSha256": "606fed71128968519719435844590c65152081ea6e18a1e6eb4f783de846fdc5", "renderVars": { "defaultBranch": "master", "baselineRef": "main", diff --git a/.github/workflows/fuze.yml b/.github/workflows/fuze.yml index c0d53ed..b37a9f2 100644 --- a/.github/workflows/fuze.yml +++ b/.github/workflows/fuze.yml @@ -1,4 +1,4 @@ -# fuze:managed template=fuze.yml baseline=main digest=sha256:6d63051eaf4c60a12997915510944b17d6fe44616b18e602950f0eb5868bf556 +# fuze:managed template=fuze.yml baseline=main digest=sha256:e9a5b6234d2b2341e1bb38ec4a68f4c4ceccac7338046f0393457dace68bca61 name: Fuze # THE CANONICAL `@fuze` ENTRYPOINT. Every onboarded repo gets this file, and this is the @@ -52,10 +52,25 @@ name: Fuze # Anthropic-or-LiteLLM leg has no working credential, this workflow fails — it does not # quietly answer from somewhere else. # -# RELATIONSHIP TO `claude.yml`. `@claude` stays as an explicit, human-typed escape hatch to -# the same routed path. `@fuze` is the family's DEFAULT handle and the one every doc, agent -# and skill now names (governance/routing.md). Both files gate independently, so a comment -# containing both mentions runs both handlers — write one or the other, not both. +# THE TRIGGER TOKEN, AND WHY THIS IS NOT A CUTOVER. Gate 1 below matches the literal string +# `@fuze`. Before this template existed, NO repo but FuzeInfra matched that token — every +# other repo's only handler was `claude.yml`, gated on `@claude` — so a comment saying only +# `@fuze` triggered nothing at all, in 21 of 22 repos, silently. That is the gap this file +# closes, and it is why the documentation change and the workflow change must land together: +# documenting `@fuze` in a repo that has not yet received THIS FILE points people at a token +# no workflow listens for, which is strictly worse than the status quo. +# +# `@claude` KEEPS WORKING, and deliberately not by being matched here. Every repo already +# carries `claude.yml`, which matches `@claude` and routes through the SAME +# fuze-code-action/llm-endpoint path this file uses — so existing issue threads and muscle +# memory continue to work untouched, and no open issue is stranded. Adding `@claude` to Gate 1 +# would NOT be a kindness: both handlers would then answer the same `@claude` mention and two +# agents would act on one thread. The two gates are deliberately DISJOINT. A comment that +# mentions both tokens does run both handlers — write one or the other. +# +# `@fuze` is the family's default handle and the one every doc, agent and skill now names +# (governance/routing.md, CLAUDE.baseline.md §9). `@claude`/`@codex`/`@gemini` remain +# human-typed escape hatches for when you deliberately want a named provider. # # SECURITY — trusted authors only. Gate 2 restricts triggering to OWNER/MEMBER/COLLABORATOR. # This handler holds a repo-write token and an agent that will push branches with it; an @@ -164,6 +179,29 @@ jobs: uses: ./.github/actions/fuze-code-action with: github-token: ${{ secrets.GITHUB_TOKEN }} + # WHERE THE GATEWAY IS, as a repo variable rather than a hardcoded literal. + # + # The default is the in-cluster service DNS name, which resolves only from a pod in + # the cluster. On the GitHub-hosted runner this job uses, the probe simply fails and + # llm-endpoint falls back to the direct vendor key — degraded but working, and the + # reason this template does not hardcode the base URL the way FuzeInfra's did. + # + # THE GATEWAY *IS* PUBLICLY REACHABLE, and pointing at it is the intended fix rather + # than a hypothetical: helm/litellm/values-contabo.yaml enables an ingress on + # `litellm.prod.fuzefront.com`. What blocks it today is Cloudflare Access, verified + # rather than assumed — `GET https://litellm.prod.fuzefront.com/v1/models` returns + # 302 to fuzefront.cloudflareaccess.com with `"service_token_status": false` and + # `"auth_status": "NONE"`. A hosted runner therefore gets a login page, not the API. + # + # Making the public host work needs TWO things this template cannot supply: + # 1. a Cloudflare Access SERVICE TOKEN — none exists (no `cloudflare_access_service_token` + # declared in FuzeInfra's IaC). Minting it is a human step; + # 2. `CF-Access-Client-Id` / `CF-Access-Client-Secret` support in + # ./.github/actions/llm-endpoint, which has no such inputs today. + # Until both land, setting this variable to the public host makes things WORSE, not + # better: the probe would get a 302 instead of a connection error and still fall + # back, with a configuration that looks deliberate. Leave it unset until then. + litellm-base-url: ${{ vars.FUZE_LITELLM_BASE_URL || 'http://litellm.fuzeinfra.svc.cluster.local:4000' }} litellm-key: ${{ secrets.LITELLM_FUZE_KEY }} anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # Inert on this path — see MENTION MODE in the header. Passed for shape-parity