Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/agents/backend-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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.
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/contract-designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `@<scope>/<svc>-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.
Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/test-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ deploy/helm/** text eol=lf
# regenerated on Linux CI and diffed, so it must be LF too.
services/custom-hostname-api/** text eol=lf
custom-hostname-client/** text eol=lf

# CLAUDE.md had somehow accumulated CRLF endings before gate-line-endings
# existed to catch it; pin it to LF so touching it again doesn't retrip the
# gate on the file's pre-existing encoding.
CLAUDE.md text eol=lf
26 changes: 26 additions & 0 deletions .github/workflows/helm-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,29 @@ jobs:
-ignore-filename-pattern 'authentik-blueprints' \
-summary \
rendered/fuzefront/templates/

- name: Guard NetworkPolicy port values (${{ matrix.values }})
run: |
# kubeconform validates SHAPE (NetworkPolicyPort.port is IntOrString) but
# not RANGE β€” a missing values.yaml default renders as `port: 0`, which is
# a syntactically valid integer, so kubeconform stays green while the live
# API server rejects it at admission time (ports must be 1-65535). That
# gap is exactly how FuzeInfra#501 shipped: a `| int` cast silently
# coerced an undefined value to 0 to satisfy kubeconform's oneOf schema,
# and every subsequent Argo sync of the whole application failed for days
# before anyone noticed. This step catches numeric out-of-range ports
# (0 or >65535) directly in the rendered manifest, with no cluster
# needed. Named ports (e.g. `port: http`, referencing a container port
# name) are valid and deliberately left unchecked.
fail=0
for f in rendered/fuzefront/templates/*.yaml; do
grep -q '^kind: NetworkPolicy$' "$f" || continue
while IFS= read -r line; do
val="${line#*: }"
if [[ "$val" =~ ^[0-9]+$ ]] && { [ "$val" -lt 1 ] || [ "$val" -gt 65535 ]; }; then
echo "::error file=$f::NetworkPolicy has invalid port value '$val' (must be 1-65535, got via a rendered default of 0 or similar) β€” see FuzeInfra#501"
fail=1
fi
done < <(grep -E '^\s*port:\s*' "$f")
done
exit $fail
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Read the baseline for the full governance model (3 layers, repo tiers, single-re
- **Backend:** Express + Postgres, with **Authentik** (identity/SSO) and **Permit** (authorization) for auth. The frontend talks to the API on a **same-origin API base** (no cross-origin base URL) so it works identically under local TLS and prod ingress β€” never hard-code an absolute API host.
- **Runs on FuzeInfra.** Deploys to Kubernetes (kind-fuzeinfra locally / Contabo k3s prod) via Helm. Infra changes are **delegated to FuzeInfra via `@claude`** β€” never edit FuzeInfra or operate the cluster from here.

## Helm values hygiene β€” don't cast around a missing default, restore it

`helm lint` and `kubeconform` are **static schema** checks: they confirm a rendered value has the right *shape*, not that it is semantically valid. This gap shipped a real outage (FuzeInfra#501): a large `values.yaml` restructuring (#523) accidentally dropped `authentik.networkPolicy.port` (and its sibling namespace keys). With the key gone, `{{ $np.port }}` rendered as nil, which correctly failed kubeconform's `oneOf: [integer, string]` schema check for `NetworkPolicyPort.port` β€” but the fix applied was `{{ $np.port | int }}`, and Sprig's `int` filter silently converts nil to `0`. `0` **is** a valid integer, so kubeconform went green β€” while the live API server correctly rejects `port: 0` (must be 1–65535) at admission time, which nothing else in CI exercises. Every Argo sync of the whole `fuzefront` Application failed for days before anyone noticed (fixed in #534).

- **A coercion filter on a `.Values.` lookup (`| int`, `| default X`, `| toString`, …) is a signal to investigate, not a fix for a lint failure.** If kubeconform/helm-lint complains about a missing or wrong-shaped value, find out *why* it's undefined before reaching for a cast. If the field is genuinely optional, declare the default explicitly in `values.yaml` where a reviewer can see it β€” don't let the template silently absorb a missing value at the render site. `gate-networkpolicy-ports` (`helm-validate.yml`) now catches the specific case of a NetworkPolicy port rendering out of range, but it does not generalize to every field a naked cast could mask.
- **A "fix missing defaults" commit whose diff is dominated by deletions is a restructuring, not an addition** β€” self-review it accordingly: diff `helm template` output for every values overlay (`values.yaml`, `values-local.yaml`, `values-prod.yaml`) before vs. after, not just the line-level YAML diff, since a reordered/consolidated file makes an eyeballed diff unreliable.
- **Two PRs touching the same top-level `values.yaml` key concurrently is the highest-risk moment for this class of bug** β€” if your branch has been open a while and merges master while another active PR is landing changes to the same key (e.g. two sibling `networkPolicy` blocks), diff exactly that region post-merge instead of trusting the auto-resolution.

## Toolchain baseline β€” Node 24 LTS / React 19 are a floor, not a suggestion

These are **minimums every manifest, image, workflow and remote must meet.** FuzeFront is the Module-Federation host, so its React major *is* the shared-singleton contract for the whole family β€” drift here does not surface as a version warning, it surfaces as a white screen in somebody else's app.
Expand Down
12 changes: 12 additions & 0 deletions deploy/helm/fuzefront/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ securityService:
googleBrokered: "true"
# Prod browser-facing Google callback URL (app host, not auth-dev).
googleRedirectUri: "https://app.fuzefront.com/api/v1/security/social/google/callback"
# Ingress NetworkPolicy allowing mendys-prod's datasets-service to reach
# fuzefront-security in-cluster (FuzeFront#493 / FuzeInfra#339). Base
# values.yaml default is OFF; enabling here is the deliberate deploy-window
# step called out in FuzeInfra#501's fix β€” the policy was never actually
# applied while Argo was stuck on the authentik-server-ingress port:0 bug.
#
# DEPLOY-WINDOW VERIFY after this syncs: from a mendys-prod pod,
# curl -sv --max-time 5 http://fuzefront-security.fuzefront.svc.cluster.local:3002/api/v1/security/session
# should reach the service (not time out / connection-refused), and confirm
# the existing Traefik + intra-fuzefront paths to fuzefront-security still work.
networkPolicy:
enabled: true

applicationsService:
enabled: true # serves /api/apps (MF app registry) β€” needed for MF apps to load
Expand Down
Loading