Problem
The shared A2A server must not know its consumers at git/build time. Today it does — twice over — via the static a2a.tenants[] block in deploy/helm/a2a-shared/values-prod.yaml:
- Routing/authz config —
values.json ConfigMap → A2A_VALUES_FILE → agent-templates/a2a/config.load_config; ServerConfig.tenant() resolves callers from that static list.
- Card-projection inputs — the
repo-sync initContainer in deploy/helm/a2a-shared/templates/deployment.yaml iterates .Values.a2a.tenants and shallow-clones each repo@ref into /repos/<tenant> so the server can project cards from the consumer's .fuze/manifest.json + agent-templates/roles/.
The runtime registry that should own this (product_registry, migration 007_cross_product_coordination.sql) lives in the orchestrator (asyncpg). The A2A server has no DB client at all. So adding a consumer today means editing this repo's git — the exact coupling to remove. Consumers should self-register at runtime into the DB; FuzeAgent stays consumer-agnostic.
Decision (settled)
- Slice-3 boundary: HTTP-to-orchestrator. The A2A server stays stateless/DB-free and fetches the tenant set from the orchestrator over HTTP; the orchestrator owns the registry DB. No second asyncpg client in the A2A server.
- Card-as-data. Consumers push their projected card (+ authz fields) at registration; the server validates it against the frozen contract and stores it. This removes server-side repo cloning entirely — the
repo-sync initContainer and the a2a-repos-git sealed token both go away (attack-surface reduction).
Slices
1 — Registry schema (orchestrator DB). New migration 009_* adding an a2a_tenants table with typed columns: tenant (PK), repo, ref, entry_role, serving_roles (jsonb/array), external (bool), provider (jsonb), card (jsonb, the consumer-projected card), enabled, registered_at, updated_at. A dedicated table, not product_registry.metadata overloading — A2A routing deserves typed columns + constraints.
2 — Registration API (orchestrator). POST /a2a/tenants/register, idempotent upsert (ON CONFLICT (tenant) DO UPDATE). Security is the crux: authorize by the caller's OIDC repo claim and reject any registration whose tenant/repo ≠ the authenticated identity (a consumer can only register itself — FuzeKeys can never register as FuzeFront). The server validates the pushed card against the frozen contracts/a2a/v1 before storing — never trust a consumer-supplied card blindly on a credential-broker-fronting surface. A read endpoint (GET /a2a/tenants) serves Slice 3.
3 — A2A server reads the registry over HTTP. agent-templates/a2a/runtime.build_from_env / config.load_config gain an HTTP registry source (orchestrator URL via env); adapter._tenant_or_none resolves from it. With card-as-data the server no longer clones consumer repos, so the repo-sync initContainer is deleted.
4 — Contract bump (contract-designer, FIRST). Moving the tenant source from the frozen values-interface to runtime registration touches contracts/a2a/v1. Additive v1.x bump freezing: the POST /a2a/tenants/register payload, the a2a_tenants shape, and the card-as-data contract. Card projection output stays byte-identical — only the source changes. This slice gates the rest.
5 — Migration/rollout (no serving outage). Dual-source (DB ∪ ConfigMap) during transition → backfill FuzeAgent/FuzeFront/FuzePlan through the new registration path → then delete static tenants[] + the repo-sync initContainer + the git token. Never a window where an enabled tenant resolves from neither.
Consumer side (tracked separately, in each product repo)
Each consumer gets a deploy-time init container (mirroring FuzeKeys' existing fuzefront-registration pattern) that POSTs its A2A registration to FuzeAgent, authenticated with its own OIDC repo token, idempotent so every ArgoCD sync re-asserts it. FuzeKeys (izzywdev/FuzeKeys, a2a.enabled: true as of #116, role keys-broker) is the first consumer and the pattern reference.
Sequencing
Slice 4 (freeze contract) → Slices 1–2 (schema + registration API) → Slice 3 (server reads registry) → Slice 5 (backfill + retire static) → consumer init containers. Slices 1–3 are independent implementation streams once the contract is frozen.
Problem
The shared A2A server must not know its consumers at git/build time. Today it does — twice over — via the static
a2a.tenants[]block indeploy/helm/a2a-shared/values-prod.yaml:values.jsonConfigMap →A2A_VALUES_FILE→agent-templates/a2a/config.load_config;ServerConfig.tenant()resolves callers from that static list.repo-syncinitContainer indeploy/helm/a2a-shared/templates/deployment.yamliterates.Values.a2a.tenantsand shallow-clones eachrepo@refinto/repos/<tenant>so the server can project cards from the consumer's.fuze/manifest.json+agent-templates/roles/.The runtime registry that should own this (
product_registry, migration007_cross_product_coordination.sql) lives in the orchestrator (asyncpg). The A2A server has no DB client at all. So adding a consumer today means editing this repo's git — the exact coupling to remove. Consumers should self-register at runtime into the DB; FuzeAgent stays consumer-agnostic.Decision (settled)
repo-syncinitContainer and thea2a-repos-gitsealed token both go away (attack-surface reduction).Slices
1 — Registry schema (orchestrator DB). New migration
009_*adding ana2a_tenantstable with typed columns:tenant(PK),repo,ref,entry_role,serving_roles(jsonb/array),external(bool),provider(jsonb),card(jsonb, the consumer-projected card),enabled,registered_at,updated_at. A dedicated table, notproduct_registry.metadataoverloading — A2A routing deserves typed columns + constraints.2 — Registration API (orchestrator).
POST /a2a/tenants/register, idempotent upsert (ON CONFLICT (tenant) DO UPDATE). Security is the crux: authorize by the caller's OIDCrepoclaim and reject any registration whosetenant/repo≠ the authenticated identity (a consumer can only register itself — FuzeKeys can never register as FuzeFront). The server validates the pushed card against the frozencontracts/a2a/v1before storing — never trust a consumer-supplied card blindly on a credential-broker-fronting surface. A read endpoint (GET /a2a/tenants) serves Slice 3.3 — A2A server reads the registry over HTTP.
agent-templates/a2a/runtime.build_from_env/config.load_configgain an HTTP registry source (orchestrator URL via env);adapter._tenant_or_noneresolves from it. With card-as-data the server no longer clones consumer repos, so therepo-syncinitContainer is deleted.4 — Contract bump (contract-designer, FIRST). Moving the tenant source from the frozen values-interface to runtime registration touches
contracts/a2a/v1. Additive v1.x bump freezing: thePOST /a2a/tenants/registerpayload, thea2a_tenantsshape, and the card-as-data contract. Card projection output stays byte-identical — only the source changes. This slice gates the rest.5 — Migration/rollout (no serving outage). Dual-source (DB ∪ ConfigMap) during transition → backfill FuzeAgent/FuzeFront/FuzePlan through the new registration path → then delete static
tenants[]+ the repo-sync initContainer + the git token. Never a window where an enabled tenant resolves from neither.Consumer side (tracked separately, in each product repo)
Each consumer gets a deploy-time init container (mirroring FuzeKeys' existing
fuzefront-registrationpattern) thatPOSTs its A2A registration to FuzeAgent, authenticated with its own OIDCrepotoken, idempotent so every ArgoCD sync re-asserts it. FuzeKeys (izzywdev/FuzeKeys,a2a.enabled: trueas of #116, rolekeys-broker) is the first consumer and the pattern reference.Sequencing
Slice 4 (freeze contract) → Slices 1–2 (schema + registration API) → Slice 3 (server reads registry) → Slice 5 (backfill + retire static) → consumer init containers. Slices 1–3 are independent implementation streams once the contract is frozen.