diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index dbcc18dd..a4bcb030 100644 --- a/.claude/agents/backend-engineer.md +++ b/.claude/agents/backend-engineer.md @@ -4,7 +4,7 @@ model: sonnet description: Implements ONLY the backend slice of a feature — HTTP API/services, business logic, DB schema/migrations, events, and the backend's own unit tests — against a frozen API contract. Does NOT build UI, the independent test suite, deploy wiring, or docs. Use for backend implementation in a contract-first fan-out. # Figma is reserved for frontend-engineer; pure-code agent gets core tools only (no MCP). tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite -skills: [api-contract-first, feature-flags, verification-protocol, model-cascade] +skills: [api-contract-first, feature-flags, logging, verification-protocol, model-cascade] --- You are a **backend engineer** for FuzeFront. You implement the **backend slice only**. diff --git a/.claude/agents/frontend-engineer.md b/.claude/agents/frontend-engineer.md index 2ccbc44a..3e8d6a02 100644 --- a/.claude/agents/frontend-engineer.md +++ b/.claude/agents/frontend-engineer.md @@ -5,7 +5,7 @@ description: Implements ONLY the UI slice of a feature — a design-system-first # SOLE owner of the Figma MCP plugin (design-to-code). All other domain agents have # Figma removed from their tool grant — it is reserved here for the UI/design-system slice. tools: "*" -skills: [fuzefront-ui-package, design-system-inheritance, design-system-conformance, ui-frame-contract, frontend-design, feature-flags, ui-runtime-validation, verification-protocol, model-cascade] +skills: [fuzefront-ui-package, design-system-inheritance, design-system-conformance, ui-frame-contract, frontend-design, feature-flags, logging, ui-runtime-validation, verification-protocol, model-cascade] --- You are a **frontend engineer**. You implement the **UI slice only**. diff --git a/agent-templates/schema/role-manifest.schema.json b/agent-templates/schema/role-manifest.schema.json index 87809834..f10670fd 100644 --- a/agent-templates/schema/role-manifest.schema.json +++ b/agent-templates/schema/role-manifest.schema.json @@ -80,6 +80,40 @@ "metadata": { "type": "object", "description": "Passed through as agent `metadata` (free-form tracking)." + }, + "a2a": { + "type": "object", + "additionalProperties": false, + "description": "OPTIONAL A2A discoverability/publication block. Mirrors the frozen contract FuzeAgent/agent-templates/contracts/a2a/v1/schema/role-a2a-extension.schema.json. Every field has a derived default, so no existing role.json needs it. The card projection reads role/name/description/services/metadata/coordinator regardless; this block only lets a role improve discoverability (examples/tags) or opt out of publication.", + "properties": { + "publish": { + "type": "boolean", + "default": true, + "description": "false hides this role from the public card. Still reachable on the EXTENDED card if the caller is allowlisted (authz.md §5)." + }, + "extendedOnly": { + "type": "boolean", + "default": false, + "description": "true publishes this skill ONLY on the authenticated extended card, never on the anonymous /.well-known/agent-card.json. Use for skills whose mere existence is sensitive." + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Extra tags merged with the derived tags. Derived tags are never removed." + }, + "examples": { + "type": "array", + "items": { "type": "string" }, + "description": "Example prompts a caller can send to this skill — the primary signal a calling agent uses to decide fit. Absent examples make a skill effectively undiscoverable." + }, + "inputModes": { "type": "array", "items": { "type": "string" } }, + "outputModes": { "type": "array", "items": { "type": "string" } }, + "scopes": { + "type": "array", + "items": { "type": "string" }, + "description": "OAuth scopes required for THIS skill, projected into the skill's securityRequirements." + } + } } } } diff --git a/frontend/e2e/post-prod/live-smoke.spec.ts b/frontend/e2e/post-prod/live-smoke.spec.ts index 0adb1f68..20342596 100644 --- a/frontend/e2e/post-prod/live-smoke.spec.ts +++ b/frontend/e2e/post-prod/live-smoke.spec.ts @@ -32,23 +32,31 @@ import { seedMockSession } from '../../tests/support/account-vault' * genuinely breaks. The API path remains as * the platform's machine/break-glass authentication. * - * Credentials come from env so we never hard-code secrets in the repo: - * POST_PROD_EMAIL / POST_PROD_PASSWORD (fallback to the documented seeded - * admin creds, which may or may not exist in prod). + * The synthetic account SELF-PROVISIONS: test 7 asks whether it exists, creates + * it if not, and signs in if it does. Nothing is provisioned by hand, and a + * rebuilt prod database heals on the next run instead of going permanently red. */ -// The seeded dev credentials are a LOCAL-ONLY fallback and must never be -// presented as a production synthetic. `initializeDatabase()` only calls -// `runSeeds()` when NODE_ENV !== 'production', so `admin@fuzefront.dev` does -// not exist in prod and never will — the authenticated journey below was -// therefore PERMANENTLY red against a healthy deploy, reported as "dashboard + -// Module-Federation apps broken" when the real cause was an unprovisioned test -// account. A test that cannot pass trains everyone to ignore red. -const HAS_SYNTHETIC_CREDS = Boolean( - process.env.POST_PROD_EMAIL && process.env.POST_PROD_PASSWORD -) -const EMAIL = process.env.POST_PROD_EMAIL || 'admin@fuzefront.dev' -const PASSWORD = process.env.POST_PROD_PASSWORD || 'admin123' +// The synthetic's ADDRESS is not a credential, so it lives here rather than in +// a secret. Fixing it in code is what makes the account stable across runs and +// discoverable when someone wonders what it is. +const EMAIL = process.env.POST_PROD_EMAIL || 'postprod-smoke@fuzefront.com' + +// The PASSWORD has no in-repo fallback, deliberately. +// +// izzywdev/FuzeFront is a PUBLIC repository. A literal here would publish a +// working production login to anyone who clones it — permanently, in git +// history — and MFA is not enforced, so the password alone would be enough to +// use it. The previous fallback (`admin123`) was survivable ONLY because +// `admin@fuzefront.dev` does not exist in prod: `initializeDatabase()` calls +// `runSeeds()` when NODE_ENV !== 'production', so the seeded admin is +// local-only. A real password with a real account behind it is a different +// thing entirely, and self-provisioning creates exactly that account. +// +// Absent → test 7 SKIPS loudly as missing coverage. It must never fall back to +// a guessable value and report green. +const PASSWORD = process.env.POST_PROD_PASSWORD +const HAS_SYNTHETIC_CREDS = Boolean(PASSWORD) // Console/network errors that indicate a real federation / mixed-content / // CSP regression. We collect these per-test and assert on them where relevant. @@ -165,27 +173,82 @@ test.describe('FuzeFront live post-prod smoke', () => { // production account to restore this coverage. test.skip( !HAS_SYNTHETIC_CREDS, - 'POST_PROD_EMAIL/POST_PROD_PASSWORD not set — no production synthetic account to sign in with. ' + - 'The seeded dev admin (admin@fuzefront.dev) does not exist in prod (seeds never run there), so this ' + - 'journey cannot be exercised. This is MISSING COVERAGE, not a passing test.' + `POST_PROD_PASSWORD not set — cannot provision or sign in as ${EMAIL}. ` + + 'The password has no in-repo fallback on purpose: this repository is public, so a literal would ' + + 'publish a working production login. This is MISSING COVERAGE, not a passing test.' ) testInfo.annotations.push({ type: 'coverage', - description: 'authenticated journey requires a provisioned production synthetic account', + description: 'authenticated journey requires the POST_PROD_PASSWORD secret', }) const { consoleErrors, pageErrors, failedRequests } = attachErrorCollectors(page) // Authenticate via the Security API (machine/break-glass path) — the same // surface the SPA uses, so this smoke fails if real sign-in is broken. - const loginResp = await request.post('/api/v1/security/session', { - data: { email: EMAIL, password: PASSWORD }, + // SELF-PROVISIONING. Ask whether the synthetic exists, create it if not, + // sign in if it does — so account creation is part of the smoke run rather + // than a manual prerequisite, and a rebuilt prod database heals itself on + // the next run instead of going permanently red. + // + // Both branches exercise a real production surface the SPA also uses: + // signup on the first run, sign-in on every run after. This still fails if + // either genuinely breaks. + const availResp = await request.get('/api/v1/security/email-available', { + params: { email: EMAIL }, }) expect( - loginResp.status(), - `POST /api/v1/security/session -> ${loginResp.status()} (401/403 = creds rejected: POST_PROD_EMAIL/POST_PROD_PASSWORD not provisioned in prod; 5xx = backend error)` + availResp.status(), + `GET /api/v1/security/email-available -> ${availResp.status()} ` + + '(429 = per-IP rate limit of 20/min, likely concurrent smoke runs; 5xx = backend error)' ).toBe(200) - const loginBody = await loginResp.json() + const { available } = await availResp.json() + + let loginBody: { status?: string; token?: string } + + if (available) { + const signupResp = await request.post('/api/v1/security/signup', { + data: { email: EMAIL, password: PASSWORD, firstName: 'Post-prod', lastName: 'Smoke' }, + }) + + if (signupResp.status() === 409) { + // Not a failure: two runners can race between the availability check + // and the signup, and the loser must fall through to signing in. + const raceLogin = await request.post('/api/v1/security/session', { + data: { email: EMAIL, password: PASSWORD }, + }) + expect( + raceLogin.status(), + `signup raced (409) and the follow-up sign-in returned ${raceLogin.status()} — ` + + 'a 401 here means the existing account has a DIFFERENT password than POST_PROD_PASSWORD' + ).toBe(200) + loginBody = await raceLogin.json() + } else { + expect( + signupResp.status(), + `POST /api/v1/security/signup -> ${signupResp.status()} while creating ${EMAIL} ` + + '(400 = password rejected by policy; 503 = signup disabled or backend down)' + ).toBe(201) + // Signup returns a LoginResponse, so a fresh account is already signed in. + loginBody = await signupResp.json() + testInfo.annotations.push({ + type: 'provisioned', + description: `created the production synthetic ${EMAIL} on this run`, + }) + } + } else { + // Steady state: the account exists, so this is the real sign-in path. + const loginResp = await request.post('/api/v1/security/session', { + data: { email: EMAIL, password: PASSWORD }, + }) + expect( + loginResp.status(), + `POST /api/v1/security/session -> ${loginResp.status()} — ${EMAIL} EXISTS but its ` + + 'credentials were rejected, so POST_PROD_PASSWORD does not match the account. Reset it in ' + + 'prod and update the secret; do NOT hard-code a password here (public repo). 5xx = backend error.' + ).toBe(200) + loginBody = await loginResp.json() + } expect( loginBody.status, 'break-glass account must not require MFA step-up, or this synthetic cannot sign in'