diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index a4bcb03..0221a83 100644 --- a/.claude/agents/backend-engineer.md +++ b/.claude/agents/backend-engineer.md @@ -16,6 +16,8 @@ HTTP API + services + business logic + DB schema/migrations + event producers/co **Pagination is mandatory on every unbounded collection endpoint** (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`). Any LIST/collection GET you implement MUST: accept `limit` (apply the contract's default + **enforce the max server-side**, clamping over-max requests) and `cursor` (preferred — opaque, server-issued, encoding sort-key + tiebreaker) or `offset`; return the envelope `{ items, page: { nextCursor|null, hasMore, total? } }`; and walk the full set deterministically (no gaps/dupes under concurrent writes). **Your unit tests assert** the limit clamp, the envelope shape, and that the cursor pages through correctly. An endpoint is exempt only if inherently bounded/singleton and so annotated in the contract (`x-pagination: exempt`). +**Identifiers are server-minted** (baseline §4.2 / `governance/identifier-standard.md`, enforced by `gate-identifier`). Never accept an `id` for a resource you are creating — mint it with `mintId()`/`mint_id()`, the only sanctioned constructor; never call `randomUUID()`/`uuid4()` for an entity id. Validate every incoming reference with `assertRef(type, id)` before use, and key polymorphic lookups on the `(type, id)` pair — never on a bare id. Type repository signatures with the branded `EntityId` so a raw string off `req.body` cannot compile; store via `toUuid()` into a native `uuid` column and render the prefixed form at the serialization boundary. **An id is never a capability** — authorization still comes from the token and the policy engine. Graph create (`lid`/`idMap`) is provided by the shared middleware: mount it and implement nothing per-route. + ## NOT your scope — never implement these (name them for the orchestrator) - **UI / frontend** (incl. any change to `design-system/` — `frontend-engineer` is its sole owner) → that's the `frontend-engineer`. - The **independent acceptance/contract test suite** → that's the `test-engineer` (API/contract) or `frontend-test-engineer` (UI e2e). You write your own unit tests, but you do NOT grade your own feature. diff --git a/.claude/agents/contract-designer.md b/.claude/agents/contract-designer.md index e6c32af..ccae156 100644 --- a/.claude/agents/contract-designer.md +++ b/.claude/agents/contract-designer.md @@ -11,7 +11,7 @@ You are the **contract designer** — the **API/event-contract lifecycle owner** ## Your scope (and ONLY this) You are the single owner of the API/event contracts — authoring, **versioning**, linting, and the generated client — not merely their initial design. From the user story / requirements (and the locked product decisions), design, freeze, and thereafter steward: -- the **HTTP API contract** — an OpenAPI/Swagger spec (resources, paths, request/response schemas, error shapes, auth scopes, **pagination per the standard**, **explicit versioning** — bump the spec version on every change and keep a changelog). **Pagination (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`):** every unbounded collection GET in the spec MUST declare `limit` (with default + max) + `cursor` (preferred, opaque) or `offset`, and the `{ items, page: { nextCursor|null, hasMore, total? } }` response envelope; mark a genuinely bounded/singleton endpoint `x-pagination: exempt` (+ `x-pagination-reason`). The contract is the single place these params/envelopes are defined so backend/test/UI all derive from one source; +- the **HTTP API contract** — an OpenAPI/Swagger spec (resources, paths, request/response schemas, error shapes, auth scopes, **pagination per the standard**, **explicit versioning** — bump the spec version on every change and keep a changelog). **Pagination (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`):** every unbounded collection GET in the spec MUST declare `limit` (with default + max) + `cursor` (preferred, opaque) or `offset`, and the `{ items, page: { nextCursor|null, hasMore, total? } }` response envelope; mark a genuinely bounded/singleton endpoint `x-pagination: exempt` (+ `x-pagination-reason`). The contract is the single place these params/envelopes are defined so backend/test/UI all derive from one source; **Identifiers (baseline §4.2 / `governance/identifier-standard.md`, enforced by `gate-identifier`):** a create body MUST NOT declare an `id`/`uuid` for the resource being created and MUST set `additionalProperties: false` — the owning service mints ids; every polymorphic reference (`entityId`, `ownerId`, `subjectId`, …) MUST carry a sibling type discriminator so no lookup resolves a bare id; a create that legitimately needs client-assigned ids is marked `x-client-assigned-id: allowed` (+ `x-client-assigned-id-reason`). Where a client must create linked entities in one request, model it as `lid` in / `idMap` out, scoped to this service's aggregate; - the **event contract** — the Kafka/AsyncAPI **Zod** event schemas + topic names/keys in the shared package, following the topic-prefix convention; - the **generated typed client** — run `openapi-typescript` to emit the `@/-client` package (private `publishConfig` + repository field), so UI, backend, and tests import the SAME types and drift becomes a compile error. **Lint the spec (Spectral)** on every revision, validate the event schemas, **version** the artifacts, regenerate the client, and **open/refresh the contract PR**. That PR — merged/frozen — is the dependency gate for the whole fan-out, and any later contract change re-enters through you, never around you. diff --git a/.claude/agents/test-engineer.md b/.claude/agents/test-engineer.md index 53212bb..10da6c1 100644 --- a/.claude/agents/test-engineer.md +++ b/.claude/agents/test-engineer.md @@ -14,6 +14,8 @@ Author the **API/service verification suite against the frozen spec** — contra **Pagination verification (mandatory).** For **every paginated endpoint in the frozen contract** (baseline §4.1 / `governance/pagination-standard.md`), your suite independently asserts: the endpoint accepts `limit` + `cursor|offset`; the response matches the `{ items, page: { nextCursor|null, hasMore, total? } }` envelope; **`limit` is enforced** (a request over the declared max is clamped, never returns more); and **the cursor walks the whole set** — paging with the returned `nextCursor` visits every item exactly once with no gaps/dupes and terminates (`nextCursor: null` / `hasMore: false`) at the end. An endpoint marked `x-pagination: exempt` is skipped (and you confirm it is genuinely bounded/singleton). +**Identifier verification (mandatory).** For every create in the frozen contract (baseline §4.2 / `governance/identifier-standard.md`), your suite independently asserts: a body carrying an `id` is **rejected** (422), not silently accepted or echoed; an id minted for one entity type is **rejected** where another type is expected (the cross-type confusion this standard exists to stop); a polymorphic reference without its type discriminator is rejected; and — the case implementers most often miss — that **knowing an id grants nothing**: a caller authorized for entity A presenting a valid id for entity B is denied. Where graph create is used, assert `idMap` covers every first-class entity created and that a `lid` naming an entity this service does not own is rejected. + ## File bugs in Jira when a test reveals a real defect A failing test against a real bug is a *valuable deliverable* — but the deliverable isn't just the red test, it's a **tracked ticket**. When your suite uncovers a genuine product defect, **file a bug in Jira** through `agile-manager`'s ticket standards: use the `ticket-creator` skill's **bug template** (and the Atlassian MCP) to create a well-formed bug — repro steps, expected vs actual, the failing test that proves it, severity, and a link back to the contract/acceptance criterion it violates. This routes the defect to the implementer (`backend-engineer` / `frontend-engineer`) instead of silently fixing it yourself. Keep the failing test in the suite so the bug stays provable until closed. diff --git a/agent-templates/sync/role_loader.py b/agent-templates/sync/role_loader.py index 9cf3685..d71f87f 100644 --- a/agent-templates/sync/role_loader.py +++ b/agent-templates/sync/role_loader.py @@ -4,9 +4,12 @@ followed by the base guardrail block and any role-specific `system_append`. """ import json +import logging import os import re +log = logging.getLogger(__name__) + HERE = os.path.dirname(os.path.abspath(__file__)) TEMPLATES_ROOT = os.path.dirname(HERE) # agent-templates/ REPO_ROOT = os.path.dirname(TEMPLATES_ROOT) # repo root (personas live under .claude/agents) @@ -76,14 +79,29 @@ def agent_payload(manifest): # or set-but-empty -> "") — the API rejects an empty/invalid url. Also drop the # matching mcp_toolset so the agent creates cleanly with only its configured servers; # re-provision after setting the URL to add the server + tool back. + # + # A drop is NEVER silent (FA-13): a required server logs at WARNING (its tools are + # missing until the URL is set), an `optional: true` server logs at INFO. A silent + # strip was the worst failure mode — an agent came up tool-less with no signal at all. servers = expand_env(manifest.get("mcp_servers", [])) valid, dropped = [], set() for s in servers: + # `optional` is our own hint, never part of the API payload — strip it either way. + optional = bool(s.pop("optional", False)) url = s.get("url", "") if url and "${" not in url: valid.append(s) + continue + name = s.get("name") + dropped.add(name) + reason = "url unset/empty" if not url else f"url unresolved ({url!r})" + if optional: + log.info("MCP server %r on agent %r dropped (optional): %s — continuing without it.", + name, manifest.get("name"), reason) else: - dropped.add(s.get("name")) + log.warning("MCP server %r on agent %r dropped: %s — its tools will be MISSING until " + "the URL is configured; re-provision after setting it.", + name, manifest.get("name"), reason) tools = expand_env(manifest.get("tools", [])) if dropped: tools = [t for t in tools diff --git a/deploy/argocd/README.md b/deploy/argocd/README.md index 66dcd41..86737ba 100644 --- a/deploy/argocd/README.md +++ b/deploy/argocd/README.md @@ -1,20 +1,62 @@ -# Argo CD wiring for FuzeAgent — a FuzeInfra delegation +# Argo CD wiring for FuzeAgent — owned by this repo -**FuzeInfra owns Argo `Application` and `AppProject` resources.** A product repo -does not author them; two repos independently declaring an Application for the -same workload is the competing-unrestricted-app failure FuzeMarket PR #61 -removed. This file is the **handoff spec**, not a manifest. +`deploy/argocd/` holds FuzeAgent's app-of-apps and its three child Applications, +and this repo owns them. FuzeInfra registers this directory once; ArgoCD +self-syncs it thereafter. -> **The manifests already in this directory are live wiring. This change -> neither adds, edits nor removes any of them.** +## Who owns what — correcting an earlier claim in this file -| Existing manifest | Deploys | +An earlier revision opened *"FuzeInfra owns Argo `Application` and `AppProject` +resources. A product repo does not author them ... This file is the handoff spec, +not a manifest."* **That is wrong**, and this directory already contradicted it — +it has carried four Application manifests the whole time, and FuzeInfra's +`argocd/applications/fuzeagent.yaml` is a *root* app-of-apps that points **at** +them, i.e. it is built on the assumption that this repo authors them. + +FuzeInfra's own onboarding contract +([`docs/CONSUMER_ONBOARDING_SHARED_CLUSTER.md`](https://github.com/izzywdev/FuzeInfra/blob/main/docs/CONSUMER_ONBOARDING_SHARED_CLUSTER.md)): + +> Boundary: the consumer owns its `deploy/**` (Helm/kustomize + Argo Applications +> + sealed secrets). FuzeInfra owns the cluster, Argo, the tunnel, and the shared +> datastores. + +FuzeInfra's `argocd-register` workflow is built around it: it takes a *"Path in +the consumer repo holding the Argo Application/AppProject manifests"*, +`kubectl apply`s the directory **once**, and then + +> After the first registration, ArgoCD polls and self-syncs the consumer's +> `deploy/argocd` manifests. + +Registration is a **one-time owner action** — never `kubectl apply` from CI or by +hand from here. + +That mistaken claim was propagated to several sibling repos in the same session +and used to delete their Applications outright (FuzeService #32, FuzeSales #44), +leaving them with no path to prod. Those deletions are being reverted. **None of +FuzeAgent's manifests were deleted**, so nothing here changes but the wording. + +## The invariant that IS real + +**One Application per workload.** FuzeContact #33 and FuzeMarket #61 removed +*duplicates*: two Applications with `prune: true` + `selfHeal: true` on the same +namespace with disagreeing values, each pruning what the other did not create. + +FuzeAgent's manifests are **not** that — the app-of-apps recurses into +`applications/`, and each child owns a **different** chart: + +| Manifest | Deploys | |---|---| | `app-of-apps.yaml` | recursive discovery of `applications/` | -| `applications/fuzeagent.yaml` | `deploy/helm/fuzeagent` — the product chart this change extends | +| `applications/fuzeagent.yaml` | `deploy/helm/fuzeagent` — the product chart | | `applications/fuzeagent-sealed.yaml` | the sealed-secret bundle | | `applications/a2a-shared.yaml` | `deploy/helm/a2a-shared` — **the family's only A2A server** | +All four use `project: fuzeagent`, the restricted AppProject FuzeInfra owns at +`argocd/projects/fuzeagent.yaml`. That project **does** exist — verified. + +> **These four manifests are live wiring. Do not delete them, and do not add a +> second Application for any chart already listed above.** + ## The A2A finding — read this first **`a2a-shared` is wired for deployment, not merely implemented.** This was the