Skip to content

feat: bring FuzeAgent to the platform's 4-pod standard - #144

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
claude/fuze-4pod-convergence
Open

feat: bring FuzeAgent to the platform's 4-pod standard#144
github-actions[bot] wants to merge 1 commit into
mainfrom
claude/fuze-4pod-convergence

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

FuzeAgent already deployed a backend (orchestrator), a frontend (ui) and the family's A2A server. What it had no contract for was the API that dispatches autonomous agents — precisely the API where an unclassified tool surface is dangerous.

🟢 The A2A question, answered

This was the open question for the whole family, so here is evidence rather than a claim. a2a-shared is wired for deployment, not merely implemented:

  • deploy/argocd/applications/a2a-shared.yaml exists on mainpath: deploy/helm/a2a-shared, targetRevision: main, valueFiles: [values-prod.yaml], namespace: fuzeagent, automated: {prune, selfHeal}.
  • values-prod.yaml on main has a2a.enabled: true and a pinned image ghcr.io/izzywdev/fuzeagent-a2a:e2d7d1c2b55a.
  • helm template against those prod values renders a Deployment and Service both named a2a-shared, port 8080, selector matching exactly one workload — i.e. exactly the address the image hardcodes on every card (card_generator.py:29, http://a2a-shared.fuzeagent.svc.cluster.local:8080/rpc). _helpers.tpl pins that name deliberately and explains why.
  • Three tenants enabled: FuzeAgent (agent-orchestrator), FuzeFront (app-shell-platform, ref master), FuzePlan (product-manager).
  • The four SealedSecrets it needs are rendered by the chart itself.

What could NOT be verified: whether the pod is actually running and healthy. No cluster access, by constraint. Everything above is GitOps declaration — the source of truth for what Argo will apply, but "declared" is not "up". Check kubectl -n fuzeagent get deploy,svc a2a-shared and its /healthz before relying on it. Also confirm a2a-repos-git can clone FuzePlan, which is private.

Minor dead value found: deploy.stateConfigMap: a2a-state is set in prod values but templates/deployment.yaml mounts an emptyDir for /state and never reads it; nothing creates that ConfigMap. Harmless, but a no-op.

No A2A pod is added anywhere. A per-product A2A Deployment is structurally unreachable — _interface() returns that module constant for every non-external card, so such a pod would publish the shared server's address and never be called.

FuzeAgent's real A2A configuration contract

Recorded here because other convergence batches are guessing at it. A repo onboards by adding a tenants[] entry to FuzeAgent deploy/helm/a2a-shared/values-prod.yaml — never a pod in its own chart.

Frozen schema: agent-templates/contracts/a2a/v1/schema/values-interface.schema.jsonadditionalProperties: false at every level, so an invented key is a hard failure. Parsed by agent-templates/a2a/config.py::load_config.

The a2a block (server-wide):

a2a:
  enabled: true                     # REQUIRED. false = server not deployed at all
  image:
    repository: ghcr.io/izzywdev/fuzeagent-a2a
    tag: <immutable>                # never `latest` in prod
    pullPolicy: IfNotPresent
  service:
    type: ClusterIP                 # const — MUST be ClusterIP
    port: 8080                      # the LISTEN port; NOT an env var
  protocolVersion: "1.0"            # const, frozen for contract v1
  auth:                             # machine-to-machine identity, NOT user identity
    oidcIssuerUrl: <required if auth present>   # token `iss` is always validated against THIS
    oidcDiscoveryUrl: <optional>    # where JWKS/discovery is FETCHED (in-cluster, avoids tunnel hairpin)
    audience: a2a
    callerClaim: repo               # the ONLY trusted caller identity
    mtls: { enabled: false, caSecretRef: {name, key} }
  cardSigning:                      # optional block; keySecretRef REQUIRED if present
    keySecretRef: { name, key }
    keyId: <jws kid>
  tenants: [ … ]                    # see below

Per-tenant entry ($defs/tenant; tenant, repo, enabled required):

- tenant: FuzeContact               # ^[A-Za-z0-9_-]+$ — MUST equal the card's AgentInterface.tenant
  repo: izzywdev/FuzeContact        # ^owner/name$ — projection source
  ref: main                         # git ref; GitOps source of truth
  enabled: true                     # per-tenant gate, independent of the server gate
  entryRole: product-manager        # ^[a-z0-9_-]+$ — overrides manifest.a2a.entryRole
  servingRoles: []                  # overrides manifest.a2a.servingRoles
  external: false                   # true ⇒ https://a2a.<repo-slug>.<domain>/rpc; MUST be false for exec tier
  provider:
    name: anthropic
    environmentId: <optional>
    apiKeySecretRef: { name, key }  # secret REFERENCES only, never values
    vaultIds: []                    # NEVER projected onto the card
    memoryResources: []
  env:
    - name: SOME_VAR
      valueFrom: { name: some-secret, key: some-key }   # secretRef only, no inline literals

Deployment mechanics live OUTSIDE a2a (in a sibling deploy: block) precisely so a2a stays byte-conformant to the frozen interface — it is serialised verbatim into the values.json the server parses: replicas, imagePullSecrets, resources, externalDomain, ingressClassName, agentProvider, gitImage, reposGitTokenSecretRef, providerApiKeySecretRef, stateConfigMap.

Container env surface (agent-templates/a2a/Dockerfile + runtime.build_from_env) — this is the entire set:

Var Meaning
A2A_VALUES_FILE /config/values.json — the {"a2a": …} document
A2A_REPOS_DIR /repos — tenant checkouts, read by LocalRepoResolver
AGENT_PROVIDER provider id, default anthropic
HOST bind address; server defaults to loopback (CWE-605-safe), chart must set 0.0.0.0
FUZE_STATE_DIR /state, must be writable
ANTHROPIC_API_KEY from providerApiKeySecretRef

The listen port is NOT an env var — the server reads a2a.service.port from the values file, so image and chart share one source.

Prerequisites in the tenant's own repo — the card is a pure projection and will fail at boot without them:

  1. .fuze/manifest.json with an a2a block (entryRole, servingRoles), and
  2. agent-templates/roles/<entryRole>/role.json.

FuzePlan has both. FuzeContact has neither (no a2a key, no agent-templates/ at all), which is why it cannot be registered yet.

Runtime surface: POST /rpc (JSON-RPC 2.0, bare PascalCase methods), GET /.well-known/agent-card.json (unauthenticated, ?tenant= selects), GET /extendedAgentCard (authenticated), GET /healthz. LocalRepoResolver reads <A2A_REPOS_DIR>/<repo-name-after-slash>, so the chart must clone each enabled tenant out of band. tools, mcp_servers, persona, model, environment and vault are never projected onto a card.

contracts/openapi.yaml — new

123 paths / 142 operations, extracted statically (ast, no imports) from every @app.<method> decorator in services/orchestrator/main.py. Summaries are the handlers' own docstrings.

🔴 Finding: seven duplicate route registrations

FastAPI keeps the first handler for a (path, method); the later one is dead code. Two are not redundant definitions but different implementations that never run: register_agent_capabilities (shadowed by register_agent) and get_agent_tasks_list (shadowed by get_agent_tasks). All seven are tabulated in contracts/README.md.

🔴 Finding: the contract covers ONE of FuzeAgent's TWO backends

services/hierarchy_API is a second FastAPI app on its own Service (port 8006) with ~40 routes. One OpenAPI document maps to one upstream base URL, so it needs its own contract and its own gateway pod. Until then those operations are not on the MCP surface — said plainly rather than reported as four green pods.

The contract is SERVED

GET /openapi.yaml is new in main.py, reading the copy baked into the image. This is not /openapi.json: FastAPI generates that from the code and it says nothing about which operations dispatch an agent that cannot be recalled. Both are served; the curated one is the contract. A missing document is degraded, not fatal/health stays 200 and gains "openapi": "loaded" | "unavailable", the endpoint answers 503.

MCP pod — and why it is not the mcpServer already deployed

The chart already runs mcp-servers/fuzeagent-server: a hand-written MCP SSE server with ~15 curated tools and, per .fuze/manifest.json's own note, no mutates classification at all. The new pod runs @fuzefront/mcp-gateway over contracts/openapi.yaml, deriving the classification mechanically and refusing to boot on a contradictory one.

That difference matters more here than anywhere else in the family. Both are left in place; whether to retire the hand-written one is an owner call, flagged rather than silently resolved.

mutates classification — on effect, never on verb

Verified, not asserted: the real gateway was booted from the exact ConfigMap bytes helm template renders. 142 tools, 74 reads / 68 writes, 16 irreversible, no tool both a read and irreversible, and startTaskExecution's description reaches the model prefixed [WRITE — IRREVERSIBLE].

Two structural facts drive the list: there is no DELETE /agents/{id} and no DELETE /tasks/{id}, and the only genuine undo in the whole contract is POST /tasks/{task_id}/file-operations/{batch_id}/rollback.

IRREVERSIBLE (16)

Dispatch / execute — POST, which defaults to reversible: startTaskExecution (hands the task to an agent that writes files, runs commands, calls external APIs; cancel stops future work and undoes nothing), initiateTaskCoordination, executeCommandInSandbox, executeContainerCommand, sendClaudeSessionInput, sendAgentCommunication, sendMessageToAgent, submitHumanResponse, deployMemoryEnabledAgent.

Creates what this API cannot remove: createAgent, createAgentFromTemplate, assignTask, storeProviderCredentials.

DELETE default, left as-is: the three delete*Document operations.

Deliberately NOT irreversible, with reasons:

  • approveFileOperations — writes files, but the contract contains the exact compensating operation (…/rollback) plus a preview. A compensating operation that actually exists is what reversible means.
  • stopMemoryEnabledAgent — a DELETE on /agents/{id}/memory that (verified in the handler) stops the agent; the exact inverse of deployMemoryEnabledAgent.
  • removeAgentContainer, destroySandbox — re-creatable runtime artefacts.
  • cancelTaskExecution / cancelCoordination — pinned reversible only so nobody reads them as the undo for startTaskExecution. They are not.

POST /rag/search is already a read via the gateway's suffix allowlist (verified mutates=false). POST /mcp/call-tool is a genuine gap: a passthrough whose reversibility belongs to the downstream tool and is not derivable from this contract. It keeps the reversible-write default, the least-wrong claim available.

Slug de-prefix — declaration only

fuzeagentagent, FuzeAgentAgent. slug is immutable; no migration code, no registry call. validate-registration passes; agent (5 chars) satisfies the ≥3-character Slug minimum.

Verified / not verified

Verified

  • helm lint --strict; helm template default / values-prod.yaml / --set mcp.enabled=true.
  • scripts/check-rendered-chart.py: 8 Services each selecting exactly one workload, no empty image refs, probes on declared ports.
  • Real gateway boot from rendered bytes (numbers above); /healthz{"product":"agent","tools":142}.
  • a2a-shared rendered with prod values and inspected (Service name, port, selector, image, volumes, env).
  • Every override key resolves to a real operationId (two were wrong on the first pass and were corrected against the spec).
  • python3 -m py_compile services/orchestrator/main.py.

NOT verified

  • No cluster access — nothing applied, and whether a2a-shared is actually running is unknown.
  • ghcr.io/izzywdev/fuze-mcp-gateway:0.1.0 is FuzeFront's image; no workflow in this repo builds it. Absent from GHCR ⇒ ImagePullBackOff, not a working pod.
  • The orchestrator test suite was not run — FastAPI and its dependency tree are not installable in this environment.
  • The live GET /openapi.yaml response was not exercised against a running app, for the same reason.
  • resourceGovernance.quota was sized for a2a-shared plus its rolling-update surge; the MCP pod fits on paper (50m/64Mi requested) but re-check before enabling.

Coordination

No auth wiring touchedservices/orchestrator/fuze_security.py, auth.py and the entire deploy/helm/a2a-shared chart are untouched, so claude/fuze-security-migration stays clean. No Argo Application added, edited or removed; deploy/argocd/README.md carries the handoff.

FuzeAgent already deployed a backend (orchestrator), a frontend (ui) and the
family's A2A server. What it had NO contract for was the API that dispatches
autonomous agents — which is exactly the API where an unclassified tool surface
is dangerous. This adds that contract, serves it, and adds the config-driven MCP
gateway pod configured from it.

contracts/openapi.yaml — NEW

123 paths / 142 operations, extracted statically (ast, no imports) from every
@app.<method> decorator in services/orchestrator/main.py. Summaries are the
handlers' own docstrings. Nothing invented.

FINDING — seven duplicate (path, method) registrations. FastAPI keeps the FIRST
and the later handler never runs. Two of them are not redundant definitions but
DIFFERENT implementations that are dead code: register_agent_capabilities
(shadowed by register_agent) and get_agent_tasks_list (shadowed by
get_agent_tasks). All seven are tabulated in contracts/README.md.

FINDING — the contract covers ONE of FuzeAgent's two backends. services/
hierarchy_API is a second FastAPI app on its own Service (port 8006) with ~40
routes. One OpenAPI document maps to one upstream base URL, so it needs its own
contract and its own gateway pod; until then those operations are not on the MCP
surface. Said plainly rather than reported as four green pods.

The contract is SERVED, not just committed

GET /openapi.yaml is new in main.py, reading the copy baked into the image. This
is NOT /openapi.json: FastAPI generates that from the code and it says nothing
about which operations dispatch an agent that cannot be recalled. Both are
served; the curated one is the contract. A missing document is DEGRADED, not
fatal — /health keeps returning 200 and gains `"openapi": "loaded" |
"unavailable"`, and the endpoint answers 503.

MCP pod — and why it is NOT the same as the mcpServer already deployed

The chart already runs mcp-servers/fuzeagent-server: a hand-written MCP SSE
server with ~15 curated tools and, per .fuze/manifest.json's own note, NO
mutates classification at all. The new pod runs @fuzefront/mcp-gateway over
contracts/openapi.yaml, which derives the classification mechanically and
refuses to boot on a contradictory one. Both are left in place; whether to
retire the hand-written one is an owner call, flagged not silently resolved.

VERIFIED, not asserted: the real gateway was booted from the exact ConfigMap
bytes `helm template` renders. 142 tools enumerate, 74 reads / 68 writes, 16
irreversible, no tool is both a read and irreversible, and startTaskExecution
reaches the model prefixed "[WRITE — IRREVERSIBLE]".

IRREVERSIBLE (16) — classified on EFFECT, never on verb:

  dispatch / execute (POST, defaults to reversible — overridden):
    startTaskExecution, initiateTaskCoordination, executeCommandInSandbox,
    executeContainerCommand, sendClaudeSessionInput, sendAgentCommunication,
    sendMessageToAgent, submitHumanResponse, deployMemoryEnabledAgent
  creates something this API cannot remove:
    createAgent, createAgentFromTemplate, assignTask, storeProviderCredentials
    (there is NO DELETE /agents/{id} and NO DELETE /tasks/{id} in the contract)
  DELETE default, left as-is:
    deleteAgentDocument, deleteOrganizationDocument, deleteTeamDocument

Deliberately NOT irreversible, with reasons in the overrides:
  approveFileOperations  writes files, but the contract contains the exact
                         compensating operation (.../rollback) plus a preview
  stopMemoryEnabledAgent a DELETE that stops an agent which can be redeployed
  removeAgentContainer / destroySandbox   re-creatable runtime artefacts
  cancelTaskExecution    pinned reversible ONLY so nobody reads it as the undo
                         for startTaskExecution. IT IS NOT.

POST /rag/search is already a read via the gateway's suffix allowlist (verified:
mutates=false), so it needs no entry. POST /mcp/call-tool keeps the default and
is flagged: it is a passthrough whose reversibility belongs to the downstream
tool and is not derivable from this contract.

Slug: fuzeagent -> agent, FuzeAgent -> Agent. DECLARATION ONLY; `slug` is
immutable and the live migration is owned elsewhere.

No A2A pod is added: A2A is one shared multi-tenant server and this repo IS its
home. deploy/argocd/README.md records the evidence that a2a-shared is wired for
deployment (Application on main, enabled:true, pinned image, Service named
a2a-shared:8080 matching the card's hardcoded URL, three tenants enabled) and is
explicit that a running healthy pod could NOT be verified without cluster access.

No Argo Application added, edited or removed — FuzeInfra owns those. No auth
wiring touched (services/orchestrator/fuze_security.py, auth.py and the whole
a2a-shared chart are untouched, leaving claude/fuze-security-migration clean).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
@izzywdev
izzywdev marked this pull request as ready for review August 3, 2026 10:58
@izzywdev
izzywdev self-requested a review as a code owner August 3, 2026 10:58
@izzywdev izzywdev added the auto-merge label Aug 3, 2026 — with Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant