Skip to content

feat(a2a): make the advertised in-cluster endpoint configurable (per-product pods) - #150

Draft
github-actions[bot] wants to merge 4 commits into
mainfrom
claude/a2a-per-product-endpoint
Draft

feat(a2a): make the advertised in-cluster endpoint configurable (per-product pods)#150
github-actions[bot] wants to merge 4 commits into
mainfrom
claude/a2a-per-product-endpoint

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

A per-product A2A pod could not be deployed, and the reason was one constant.
card_generator.IN_CLUSTER_URL was returned by _interface() as the callable
endpoint of every non-external Agent Card, so any second deployment started,
passed every probe, served a valid signed card — and that card named the SHARED
server. A caller doing the right thing (fetch card, follow
supportedInterfaces[0].url) was routed to a pod that had never heard of it.
Nothing was unhealthy; the pod was simply undiscoverable through its own card.
That is why every product's A2A pod shipped disabled.

Contract v1.2.0 (additive MINOR) adds optional a2a.inClusterUrl, defaulting to
the current constant. The values interface is additionalProperties: false at
every level, so this is a deliberate, versioned schema change.

It goes INSIDE the a2a block, not the sibling chart-internal deploy: block,
because a2a is what the chart serialises verbatim into the values.json the
server parses (config.load_config) — a key under deploy: never reaches the
server and would need an invented env var to get there. Substantively it is a
card-projection input (the published callable endpoint, like
auth.oidcIssuerUrl), not a chart knob like replicas. No new env var: the
container surface is unchanged.

  • card_generator: DEFAULT_IN_CLUSTER_URL (+ IN_CLUSTER_URL back-compat alias);
    in_cluster_url threaded through _interface / project_product_card /
    project_exec_card / generate_cards. Ignored for external tenants.
  • config: ServerConfig.in_cluster_url parsed from a2a.inClusterUrl; empty
    string treated as unset so a half-templated value can never publish an empty URL.
  • adapter: passes it into every card it projects, product and exec.

No regression to a2a-shared (live tenants FuzeAgent, FuzeFront, FuzePlan): the
prod values file does not set the key, so the card is byte-identical. Verified by
test and by diffing helm template for values.yaml / values-prod.yaml — the only
render change is a corrected YAML comment on the Service.

Single-tenant mode needed no code change: server._card_tenant already infers the
sole enabled tenant when ?tenant= is absent, the session store is per-pod and
caller-scoped, and LocalRepoResolver keys on the tenant name. RPC still requires
params.tenant — correct and fail-closed, since the card always carries
AgentInterface.tenant and the shipped A2AClient echoes it; locked by a test
that documents it, because the rejection is deliberately indistinguishable from an
authz denial.

Tests: 161 pass (was 145). Contract conformance: 68 pass. helm lint clean, and
the rendered values.json validates against the frozen schema and parses.

Docs: new docs/a2a/per-product-pod.md — the values a product chart must set, the
secrets (SealedSecrets are namespace-scoped and NOT portable from fuzeagent), the
pod shape, and the two repo files without which the projection fails at boot
(.fuze/manifest.json with providesTo, roles/<entryRole>/role.json with a
description).

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv

izzywdev and others added 4 commits August 10, 2026 17:33
…product pods)

A per-product A2A pod could not be deployed, and the reason was one constant.
`card_generator.IN_CLUSTER_URL` was returned by `_interface()` as the callable
endpoint of every non-external Agent Card, so any second deployment started,
passed every probe, served a valid signed card — and that card named the SHARED
server. A caller doing the right thing (fetch card, follow
`supportedInterfaces[0].url`) was routed to a pod that had never heard of it.
Nothing was unhealthy; the pod was simply undiscoverable through its own card.
That is why every product's A2A pod shipped disabled.

Contract v1.2.0 (additive MINOR) adds optional `a2a.inClusterUrl`, defaulting to
the current constant. The values interface is `additionalProperties: false` at
every level, so this is a deliberate, versioned schema change.

It goes INSIDE the `a2a` block, not the sibling chart-internal `deploy:` block,
because `a2a` is what the chart serialises verbatim into the `values.json` the
server parses (`config.load_config`) — a key under `deploy:` never reaches the
server and would need an invented env var to get there. Substantively it is a
card-projection input (the published callable endpoint, like
`auth.oidcIssuerUrl`), not a chart knob like `replicas`. No new env var: the
container surface is unchanged.

- card_generator: `DEFAULT_IN_CLUSTER_URL` (+ `IN_CLUSTER_URL` back-compat alias);
  `in_cluster_url` threaded through `_interface` / `project_product_card` /
  `project_exec_card` / `generate_cards`. Ignored for `external` tenants.
- config: `ServerConfig.in_cluster_url` parsed from `a2a.inClusterUrl`; empty
  string treated as unset so a half-templated value can never publish an empty URL.
- adapter: passes it into every card it projects, product and exec.

No regression to `a2a-shared` (live tenants FuzeAgent, FuzeFront, FuzePlan): the
prod values file does not set the key, so the card is byte-identical. Verified by
test and by diffing `helm template` for values.yaml / values-prod.yaml — the only
render change is a corrected YAML comment on the Service.

Single-tenant mode needed no code change: `server._card_tenant` already infers the
sole enabled tenant when `?tenant=` is absent, the session store is per-pod and
caller-scoped, and `LocalRepoResolver` keys on the tenant name. RPC still requires
`params.tenant` — correct and fail-closed, since the card always carries
`AgentInterface.tenant` and the shipped `A2AClient` echoes it; locked by a test
that documents it, because the rejection is deliberately indistinguishable from an
authz denial.

Tests: 161 pass (was 145). Contract conformance: 68 pass. `helm lint` clean, and
the rendered `values.json` validates against the frozen schema and parses.

Docs: new `docs/a2a/per-product-pod.md` — the values a product chart must set, the
secrets (SealedSecrets are namespace-scoped and NOT portable from `fuzeagent`), the
pod shape, and the two repo files without which the projection fails at boot
(`.fuze/manifest.json` with `providesTo`, `roles/<entryRole>/role.json` with a
description).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
This branch's head was pushed by an identity whose pushes do not trigger
workflows, so the PR had ZERO checks and could never satisfy branch
protection - nothing red to investigate, just nothing at all. A commit from
a collaborator-backed identity produces a synchronize event with a real
pusher, which is what actually starts CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant