Skip to content

A2A: resolve tenants from a runtime DB registry, not the static values ConfigMap #203

Description

@izzywdev

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:

  1. Routing/authz configvalues.json ConfigMap → A2A_VALUES_FILEagent-templates/a2a/config.load_config; ServerConfig.tenant() resolves callers from that static list.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions