diff --git a/.github/actions/llm-endpoint/action.yml b/.github/actions/llm-endpoint/action.yml index 78041e15..ab3f22e8 100644 --- a/.github/actions/llm-endpoint/action.yml +++ b/.github/actions/llm-endpoint/action.yml @@ -281,11 +281,32 @@ runs: openai) model="$FALLBACK_OPENAI_MODEL"; keyref='os.environ/OPENAI_API_KEY' ;; gemini) model="$FALLBACK_GEMINI_MODEL"; keyref='os.environ/GEMINI_API_KEY' ;; esac + # Remember the head vendor's route for the "*" catch-all below. + [ -z "${head_model:-}" ] && head_model="$model" && head_keyref="$keyref" echo " - model_name: ${v}" echo " litellm_params:" echo " model: ${model}" echo " api_key: \"${keyref}\"" done + # CATCH-ALL. The vendor-named entries above are only reachable by a + # caller that knows to ask for "anthropic". claude-code-action does + # NOT: it sends its own model name (e.g. claude-opus-4-8), which + # matches nothing here, so LiteLLM answers + # 400 {'error': 'completion: Invalid model name passed in + # model=claude-opus-4-8'} + # and the rung writes that string as its assistant text, reports NO + # conclusion, and still exits 0. Downstream that is indistinguishable + # from "the model declined to work", so fuze-code-review abstained and + # went red on every PR that took the runner-local path -- while the + # job's own message blamed the workflow-self-modification guard, which + # was not involved (observed on FuzeInfra#823, a one-file Helm change). + # + # "*" routes any unrecognized model name to the HEAD vendor, so the + # chain works without every caller having to pass --model. + echo " - model_name: \"*\"" + echo " litellm_params:" + echo " model: ${head_model}" + echo " api_key: \"${head_keyref}\"" echo "" echo "router_settings:" echo " num_retries: 0" @@ -301,6 +322,10 @@ runs: rest_yaml="${rest// /\", \"}" echo " fallbacks:" echo " - ${head}: [\"${rest_yaml}\"]" + # The catch-all must roll over too, otherwise a caller that does not + # name a vendor (claude-code-action) gets a single-vendor chain and + # an exhausted head key fails the run instead of advancing. + echo " - \"*\": [\"${rest_yaml}\"]" fi echo "" echo "general_settings:" diff --git a/deploy/helm/fuzefront/values-prod.yaml b/deploy/helm/fuzefront/values-prod.yaml index 2540338f..f7836a49 100644 --- a/deploy/helm/fuzefront/values-prod.yaml +++ b/deploy/helm/fuzefront/values-prod.yaml @@ -259,21 +259,33 @@ mcp: # # `a2a-repos-git` is NOT needed: FuzeFront is a PUBLIC repo (verified via the # GitHub API), so the repo-sync init container clones anonymously. -# `ghcr-pull` is NOT needed either: ghcr.io/izzywdev/fuzeagent-a2a is a public +# `ghcr-pull` is NOT needed either: ghcr.io/izzywdev/fuze-a2a is a public # GHCR package (verified — anonymous token exchange + manifest GET succeeded). a2a: enabled: true image: - repository: ghcr.io/izzywdev/fuzeagent-a2a - # `tag` alone would be a mutable moving target (image reference grammar - # allows `tag@digest` together — kubelet/containerd resolve via the - # digest, so this is fully immutable regardless of what `latest` is - # repointed to later; the `tag` half is cosmetic/readable only). Digest - # verified anonymously pullable 2026-08-23 (what - # ghcr.io/izzywdev/fuzeagent-a2a:latest resolved to at verification time - # — `latest` itself is never dereferenced at deploy time, only used to - # locate this digest). Re-pin when FuzeAgent's CI publishes a new build. - tag: "latest@sha256:c233bb47409300e769c68f84c12077e3a74db132f287fa218929742fbe48ccf6" + # CORRECTED 2026-09-02: ghcr.io/izzywdev/fuzeagent-a2a (this pin's prior + # value) is a stale/abandoned image — last pushed 2026-08-24, still + # anonymously pullable but no longer built by anything. FuzeAgent's own + # release.yml ("Build & push a2a-shared" step) builds + # agent-templates/a2a/Dockerfile — the ONE shared A2A runtime — and + # publishes it ONLY as ghcr.io/izzywdev/fuze-a2a:{sha,latest}; that is + # also what FuzeAgent's own deploy/helm/a2a-shared/values-prod.yaml points + # at. (gate-a2a's own I1 default + the runtime standard doc still name + # `fuzeagent-a2a`; that is already-flagged gate/doc staleness, FuzeAgent + # PR #257 — an I1 finding against this file is that, not a new fork.) + repository: ghcr.io/izzywdev/fuze-a2a + # Plain CI-built SHA tag (not a `tag@digest` compound — that form is a + # valid Docker/OCI image-reference grammar, but gate-a2a's registry check + # GETs the literal `tag` string as the manifest reference and 404s on the + # compound form, which is what caused the prior pin to read as + # unpullable even though the underlying digest was fine). Matches the + # tag FuzeAgent's own release.yml currently has live in + # deploy/helm/a2a-shared/values-prod.yaml. Verified anonymously pullable + # 2026-09-02 (anonymous GHCR token + manifest GET → HTTP 200, + # digest sha256:26b84ab9c04844c685b0312b660e934232338f79f5b86001fc9e2520d4a3b9a6). + # Re-pin when FuzeAgent's CI publishes a new build. + tag: "624956d8983b" pullPolicy: IfNotPresent service: type: ClusterIP diff --git a/deploy/helm/fuzefront/values.yaml b/deploy/helm/fuzefront/values.yaml index 70a41c2c..ff62e0d0 100644 --- a/deploy/helm/fuzefront/values.yaml +++ b/deploy/helm/fuzefront/values.yaml @@ -385,12 +385,23 @@ mcp: a2a: enabled: false image: - repository: ghcr.io/izzywdev/fuzeagent-a2a - # Immutable; never `latest` in prod (values-prod.yaml pins by digest, - # encoded as `tag@digest` — see that file's comment). Left empty here - # (local/dev default; server disabled) so `helm template` with the base - # values alone never renders an unpinned image reference. - tag: "" + # ghcr.io/izzywdev/fuzeagent-a2a is a stale/abandoned image (last pushed + # 2026-08-24; anonymously pullable but no longer built). The image + # FuzeAgent's own release.yml actively builds and publishes on every + # release, and what its own a2a-shared/values-prod.yaml points at, is + # ghcr.io/izzywdev/fuze-a2a (agent-templates/a2a/Dockerfile). Corrected + # here 2026-09-02 — gate-a2a's own I1 default + the runtime standard doc + # still name the old `fuzeagent-a2a` repository (already flagged, + # FuzeAgent PR #257); a resulting I1 finding against this file is that + # known staleness, not a new fork. + repository: ghcr.io/izzywdev/fuze-a2a + # `latest` is the permitted dev-shape tag (forbidden only in + # values-prod.yaml, which pins an exact CI-built tag) — matches + # FuzeAgent's own a2a-shared/values.yaml base shape. A blank tag here + # previously made gate-a2a report the image as "not fully declared" even + # though the server is disabled; `latest` keeps the declaration complete + # and verifiable while still describing the disabled dev default. + tag: "latest" pullPolicy: IfNotPresent service: # MUST be ClusterIP — ingress is Cloudflare-tunnel-only, and `inClusterUrl` @@ -402,11 +413,17 @@ a2a: # Unset would default to the SHARED server's card URL and this pod would be # unreachable through its own card despite passing every probe. inClusterUrl: "" - auth: - oidcIssuerUrl: "" - oidcDiscoveryUrl: "" - audience: a2a - callerClaim: repo + # `auth` is left UNSET (not an empty-valued object) in this disabled base + # shape on purpose: the frozen interface requires `oidcIssuerUrl` whenever + # `auth` is present at all, so a present-but-empty block is a gate-a2a C3 + # fatal finding on a file that was never meant to carry a real issuer. + # Mirrors FuzeAgent's own a2a-shared/values.yaml base shape. values-prod.yaml + # supplies the real `auth:` block for the enabled deployment. + # auth: + # oidcIssuerUrl: "" + # oidcDiscoveryUrl: "" + # audience: a2a + # callerClaim: repo cardSigning: keySecretRef: { name: "", key: "" } keyId: a2a-v1 diff --git a/docs/a2a/README.md b/docs/a2a/README.md index 0580463a..56e428c1 100644 --- a/docs/a2a/README.md +++ b/docs/a2a/README.md @@ -1,5 +1,30 @@ # FuzeFront's A2A surface — current state and what's left +> **Status update 2026-09-02 (a2a-maintainer verification pass).** Section 2 +> below ("Deployment — devops-engineer") is **done**, not still open: +> `deploy/helm/fuzefront/templates/a2a.yaml` + the `a2a:` block in +> `values.yaml`/`values-prod.yaml` exist, `helm lint`/`helm template` clean, +> and `gate-a2a` passes except for two already-flagged, known-stale findings +> (see below). Also corrected in this pass: every reference below to +> `ghcr.io/izzywdev/fuzeagent-a2a` names a now-**abandoned** image (last +> pushed 2026-08-24, still anonymously pullable but no longer built by +> anything); the chart now correctly points at `ghcr.io/izzywdev/fuze-a2a`, +> which is what `izzywdev/FuzeAgent`'s `release.yml` actually builds and +> publishes today, and what its own `a2a-shared/values-prod.yaml` uses. Left +> the historical narrative below as written (it was accurate when this +> document was authored) rather than rewriting it. +> +> **Open cross-repo question, not resolved here (out of this agent's scope — +> single-repo, no cross-repo architecture calls):** `izzywdev/FuzeAgent`'s +> `deploy/helm/a2a-shared/values-prod.yaml` also lists a `FuzeFront` tenant +> entry (currently behind that file's own "STILL DO NOT MERGE" GO-LIVE gate). +> If that shared-server rollout proceeds while this repo's own per-product +> pod stays `enabled: true`, the same tenant name would be served by two +> different pods — the exact ambiguity `a2a-runtime-standard.md` §2 exists to +> prevent. Whoever runs that GO-LIVE needs to either drop the `FuzeFront` +> tenant entry from `a2a-shared` or disable this repo's own pod first; +> flagged for `platform-governance`/`devops-engineer`, not decided here. + **There is no A2A server in this repo, and there should not be one.** The shared A2A runtime (Claude-driven, one image, zero product logic — system prompt, skills, and tool access all arrive as mounted/env configuration) already diff --git a/governance/a2a-policy.json b/governance/a2a-policy.json new file mode 100644 index 00000000..da002139 --- /dev/null +++ b/governance/a2a-policy.json @@ -0,0 +1,41 @@ +{ + "$comment": "Ratchet policy for gate-a2a (scripts/gate_a2a.py). See governance/a2a-runtime-standard.md §4 (FuzeSDLC). IF THIS FILE IS ABSENT THE GATE RUNS IN `fail` MODE — absence is not permission. Only the named worklist below is softened, and only for the rules that name it; a repo not on a worklist gets `fail` even while the mode reads `warn`. Seeded (not managed) from FuzeSDLC governance/a2a-policy.json — governance-sync seeds this once per repo and does not overwrite it on later syncs, because the ratchet worklist and the sealedSecretDirs customization below are this repo's own record of debt and layout, not fleet-wide config.", + "version": 1, + "skills": { + "adoption": "warn", + "$comment": "Rule 4.1 (a NAMED skill that does not resolve to .claude/skills//SKILL.md) ALWAYS fails and is not governed by this key — a new violation can never be ratcheted past. This key governs rule 4.2 (a2a.enabled repo whose serving roles declare ZERO bundle skills) and 4.3 (missing root CLAUDE.md). FuzeFront's app-shell-platform role currently declares zero bundle skills: none of this repo's .claude/skills/* bundles document the role's actual product knowledge (Module-Federation host shell, app registration/discovery, health/heartbeat, shell surface) — they are generic SDLC-process skills, not app-shell-platform domain knowledge, so none was claimed here rather than force-fitting an unrelated bundle name. CLAUDE.md itself is present at the repo root (rule 4.3 satisfied). Flip to `fail` once ratchet.knownUnadopted is empty.", + "ratchet": { + "owner": "@izzywdev", + "$comment": "Repos with a2a.enabled=true and zero bundle skills declared on their serving roles, measured on their LIVE default branch 2026-08-23 (FuzeSDLC governance/a2a-policy.json, unmodified list). Claiming FuzeFront's entry means giving app-shell-platform a real skills[] list pointing at a real .claude/skills//SKILL.md bundle that documents its actual domain behaviour — product/domain authorship, owned by frontend-engineer/backend-engineer, not a2a-maintainer. Removing the last entry is the trigger to set adoption=fail.", + "knownUnadopted": [ + "izzywdev/FuzeFront", + "izzywdev/fuzeagent", + "izzywdev/fuzebi", + "izzywdev/fuzehub", + "izzywdev/fuzeplan", + "izzywdev/fuzesales", + "izzywdev/fuzeservice", + "izzywdev/fuzex" + ] + } + }, + "image": { + "repository": "ghcr.io/izzywdev/fuzeagent-a2a", + "$comment": "Unmodified from the FuzeSDLC canonical on purpose — this is the fleet-wide default and gate_a2a.py's I1 check compares every repo's declared a2a.image.repository against it. It is KNOWN STALE (ghcr.io/izzywdev/fuzeagent-a2a was abandoned 2026-08-24; the image FuzeAgent's release.yml actually builds and publishes today is ghcr.io/izzywdev/fuze-a2a — already flagged, FuzeAgent PR #257). FuzeFront's own values.yaml/values-prod.yaml correctly declare fuze-a2a; the resulting I1 finding here is that known, already-reported gate/doc staleness, not a real fork — do not silently point this repo back at the stale name just to make I1 pass." + }, + "creds": { + "sealedSecretDirs": ["deploy/contabo/sealed", "deploy/sealed-secrets", "helm/sealed-secrets", "deploy/helm/sealed-secrets"], + "$comment": "Added `deploy/contabo/sealed` ahead of the FuzeSDLC-canonical default list: that is FuzeFront's ACTUAL SealedSecrets directory (deploy/contabo/sealed/a2a-fuzefront-secrets.yaml carries a2a-card-signing/jws.key; deploy/contabo/sealed/fuzefront-secrets.yaml carries LITELLM_MASTER_KEY) — the canonical default (deploy/sealed-secrets) does not exist as a secrets tree in this repo (it holds unrelated Permit.io policy files) and gate-a2a reported both real secretRefs as unwired until this was added.", + "externallyProvisioned": { + "$comment": "secretRef {name} values provisioned OUTSIDE this repo's git tree. Each entry MUST name who provisions it — an unexplained entry is treated as missing. The gate NEVER reads a secret VALUE from anywhere; these names exist only so a legitimately-external secret is not reported as unwired." + } + }, + "memory": { + "mode": "client-only", + "$comment": "The A2A pod is a Chroma CLIENT of the family's existing Chroma service, using a per-tenant collection. It never runs a Chroma server: chromadb carries PYSEC-2026-311 (unfixable pre-auth code injection in the SERVER's collections handler via trust_remote_code), and FuzeAgent is unaffected only because it never serves that endpoint. See governance/a2a-runtime-standard.md §7 (FuzeSDLC)." + }, + "apiSurface": { + "mode": "mcp-only", + "$comment": "The A2A pod reaches FuzeFront's API through FuzeFront's own MCP gateway (packages/mcp-gateway, per .fuze/manifest.json mcp block) pointed at the product's FULL OpenAPI document. A raw-REST fallback is REFUSED, not deferred — see governance/a2a-runtime-standard.md §8 (FuzeSDLC)." + } +}