From 96fd7992e83f0d2dbdcf0c9a1edb634dd3858d88 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 11:04:12 +0000 Subject: [PATCH 1/5] feat(registration): self-register FuzeAgent with the FuzeFront portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FuzeAgent's portal presence is currently a HARDCODED entry in FuzeFront's backend/applications/src/app-registry/builtins.ts — FuzeFront's source has to change for FuzeAgent's own menu entry to change. This moves that declaration here. - registration/manifest.json — identity + `nav: { section: "build", order: 10 }`, leading the build stage. Menu order was previously unexpressible: the registry sorted by created_at, so the side menu was in registration order. integration.type is genuinely `module-federation` here, unlike most siblings. The contract is copied from services/ui-react/vite.config.ts and verified to match: scope=fuzeagentApp, module=./FuzeAgentApp, remoteEntry at the prod host. - registration/policy.json — derived from services/orchestrator/models.py: Organization, Team, Agent, Task, plus Goal (the organizational-goal surface the MCP server exposes). Agent:deploy and Task:assign are split from ordinary writes because they cause an autonomous agent to actually RUN — a different kind of authority from editing a record. - .fuze/manifest.json: * mcp block with enabled:TRUE — this is not a scaffold. mcp-servers/fuzeagent-server/ already serves 15+ tools against the hierarchy API. The note names the tools that MUST be classified mutates:true (assign_task, deploy_agent, create_custom_agent, update_task_status, create_organizational_goal, update_goal_progress, create_goal_conversation) and flags the missing tools.json for mcp-maintainer. * a2a block naming the REAL serving role `agent-orchestrator` — agent-templates/a2a/ implements card_generator/adapter/authz/identity and the role is fully described. Left enabled:false because flipping it requires verifying the card projects schema-valid, which is a2a-maintainer's call, not a manifest edit's. Validated: manifest passes the generated AppManifest schema; the MF scope/module match vite.config.ts exactly; every permission resolves to a resource+action the same document declares. NOT DONE, deliberately: the init container is not wired. deploy/helm/fuzeagent exists but this is a multi-service deployment and exactly one deployment must register — wiring more than one would have them race and duplicate-register. Picking the owner is devops-engineer's call, flagged not guessed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .claude/agents/mcp-maintainer.md | 85 ++++++++++++++ .fuze/manifest.json | 20 ++++ .github/workflows/mcp-maintain.yml | 104 +++++++++++++++++ registration/README.md | 82 +++++++++++++ registration/manifest.json | 21 ++++ registration/policy.json | 82 +++++++++++++ registration/register.sh | 180 +++++++++++++++++++++++++++++ 7 files changed, 574 insertions(+) create mode 100644 .claude/agents/mcp-maintainer.md create mode 100644 .github/workflows/mcp-maintain.yml create mode 100644 registration/README.md create mode 100644 registration/manifest.json create mode 100644 registration/policy.json create mode 100755 registration/register.sh diff --git a/.claude/agents/mcp-maintainer.md b/.claude/agents/mcp-maintainer.md new file mode 100644 index 0000000..82d5273 --- /dev/null +++ b/.claude/agents/mcp-maintainer.md @@ -0,0 +1,85 @@ +--- +name: mcp-maintainer +model: sonnet +description: Keeps a repo's MCP (Model Context Protocol) surface current and correct, running as part of CI. Detects whether anything changed that requires the repo's MCP server to be built (first time) or updated (drift) — the `.fuze/manifest.json` mcp block, the server under `mcp/`, its tool manifest, and conformance to the frozen MCP contract — and if so makes the change and pushes it to the PR (or opens a separate auto-mergeable follow-up PR). Does NOT author a tool's product/domain behaviour, handle prod credentials, or deploy. Use as the automated MCP upkeep stream. +tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite +skills: [verification-protocol, model-cascade] +--- + +You are the **MCP maintainer**. You keep this repo's **Model Context Protocol surface** +current — the sibling of `a2a-maintainer`, and deliberately shaped the same way. A2A is +how *agents* ask this repo for an outcome; **MCP is how an LLM session queries and +operates on this repo's objects and data directly**. You run automatically in CI on +every PR (and can be `@`-invoked). You are **upkeep, not product**: you wire the +surface, you never invent what a tool *does*. + +## What "the MCP surface" is (the only things you own) + +1. **`.fuze/manifest.json` `mcp` block** — `enabled`, `servers[]` (each with `name`, + `transport`, `entry`), `entryServer`. Present and internally consistent for a repo + that means to be drivable from an LLM session. +2. **The server itself** — `mcp/server.` exists for every server named in + `servers[]`, starts, and advertises a tool list. +3. **The tool manifest** — `mcp/tools.json` describing every exposed tool: `name`, + `description`, `inputSchema`, and **`mutates: true|false`**. A tool missing + `mutates` is a break (see the read/write split below). +4. **Contract currency** — the pinned MCP protocol version matches what the family + standard declares. + +## First run vs drift + +- **First time:** scaffold it — add the `mcp` block (default `enabled: false`), create + a minimal server skeleton that starts and lists zero tools, and `TODO`-mark every + tool the repo obviously needs but whose behaviour you must not invent. +- **Drift:** reconcile only what changed — a renamed server, a tool added to the code + but missing from `tools.json`, a schema that no longer matches the handler, a + protocol-version bump. Touch the minimum. +- **In sync:** do nothing and say so. Silence when correct is the goal; do not churn. + +## The read/write split — the one judgement call you MUST NOT skip + +Every tool declares `mutates`. When you scaffold or reconcile a tool, classify it, and +where a repo exposes anything sensitive, **flag rather than decide**: + +- **Secret/credential material** (e.g. FuzeKeys): listing, describing, and rotating a + key are ordinary tools. A tool that returns raw secret **material** puts plaintext + into session transcripts, so it must be its own explicitly-named tool — never a + field that falls out of a `list` or `describe` response. If you find material + returned as a side effect of a read, do not silently redesign it: `NEEDS PRODUCT` + it. +- **Infrastructure** (e.g. FuzeInfra): reads are ordinary. A mutating tool must drive + the repo's own GitOps path — under Argo `selfHeal` a direct cluster patch is + reverted, so a `kubectl`-shaped write tool is broken by construction, not merely + risky. Flag it. + +You classify and flag. You do not get to decide a product's exposure policy. + +## Output — push to the PR, else a follow-up PR + +- **On a PR (same-repo):** commit back to the PR branch so the MCP surface lands *with* + the change that affected it. Prefix `chore(mcp-maintain):` and end the commit + `[skip mcp]` so you never re-trigger yourself. +- **When you can't push** (fork PR, or a first-time build on a `push` run): open a + follow-up PR from an `mcp-maintain/**` branch, labelled **`auto-merge`**. Never + self-merge. + +## Hard boundaries (flag, never fabricate) + +- **Never author a tool's product/domain behaviour** — what it actually queries, which + table it reads, its real business rules. Scaffold a schema-valid skeleton and + `BLOCKED:`/`TODO` the behaviour for the owning product agent. +- **Never handle credentials or secrets**, never `kubectl`, never touch prod. +- **Never flip `enabled: true`** for a server with no working tool. An advertised + server that errors on every call is worse than one that is off. +- **Never widen a tool's `mutates: false` to `true`** to make a handler compile. If the + handler mutates, the classification was right and the *handler* is the bug. + +## Done contract (report exactly this) + +`MCP SURFACE: ` — then either +`SCOPE DONE (verified): ` +or `NO CHANGE — in sync`. Always append +`NEEDS PRODUCT/OPERATOR: ` +when the surface can't be fully live without them. Verify the server actually starts +and lists its tools before claiming done — a server that doesn't start is a bug, not a +deliverable. diff --git a/.fuze/manifest.json b/.fuze/manifest.json index 03fcb93..259e44f 100644 --- a/.fuze/manifest.json +++ b/.fuze/manifest.json @@ -29,6 +29,26 @@ "requireSignatures": true, "deployOnPush": false }, + "a2a": { + "enabled": false, + "servingRoles": [ + "agent-orchestrator" + ], + "entryRole": "agent-orchestrator", + "note": "The A2A surface here is REAL, not a scaffold: agent-templates/a2a/ implements card_generator, adapter, authz and identity, and agent-templates/roles/agent-orchestrator/role.json is a fully-described serving role. Left enabled:false only because flipping it requires verifying the card projects schema-valid against the frozen contract \u2014 a2a-maintainer's call, not a manifest edit's." + }, + "mcp": { + "enabled": true, + "servers": [ + { + "name": "fuzeagent", + "transport": "stdio", + "entry": "mcp-servers/fuzeagent-server/server.py" + } + ], + "entryServer": "fuzeagent", + "note": "REAL, not a scaffold \u2014 mcp-servers/fuzeagent-server/ already serves 15+ tools over the hierarchy API. Outstanding: a tools.json declaring `mutates` per tool. Several tools mutate (assign_task, deploy_agent, create_custom_agent, update_task_status, create_organizational_goal, update_goal_progress) and MUST be classified mutates:true; the read tools (list_*, get_*) are mutates:false. Classification is mcp-maintainer's to reconcile." + }, "providesTo": [ "Exec-ceo", "Exec-cfo", diff --git a/.github/workflows/mcp-maintain.yml b/.github/workflows/mcp-maintain.yml new file mode 100644 index 0000000..0498686 --- /dev/null +++ b/.github/workflows/mcp-maintain.yml @@ -0,0 +1,104 @@ +name: MCP Maintain + +# Keeps this repo's Model Context Protocol (MCP) surface current, as part of CI. The +# sibling of a2a-maintain.yml and deliberately shaped the same way: on every PR it runs +# the `mcp-maintainer` agent to check whether anything changed that requires the repo's +# MCP server to be BUILT (first time) or UPDATED (drift) — the `.fuze/manifest.json` +# mcp block, the server under `mcp/`, its tools.json, and protocol-version currency. +# If so it commits the change BACK TO THE PR BRANCH; when it can't (fork PR / first-time +# build), it opens a separate `auto-merge`-labelled follow-up PR. It never fabricates a +# tool's product behaviour, never handles creds, never deploys. +# +# WHY MCP is maintained separately from A2A: A2A is how another AGENT asks this repo for +# an outcome; MCP is how an LLM SESSION queries and operates on this repo's objects and +# data directly. Different surfaces, different contracts, different failure modes — so +# one agent per surface rather than one that half-understands both. +# +# Requires repo secret ANTHROPIC_API_KEY. No key -> SKIPS (never red). + +on: + pull_request: + +permissions: + contents: write # commit the reconciled MCP surface back to the PR branch + pull-requests: write # open a follow-up PR when a push-back isn't possible + id-token: write # claude-code-action fetches an OIDC token (required) + actions: read + +concurrency: + group: mcp-maintain-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + mcp-maintain: + # Loop guard: never run on our own maintenance branches. + if: ${{ !startsWith(github.event.pull_request.head.ref, 'mcp-maintain/') }} + runs-on: ubuntu-latest + steps: + - name: Skip if key absent + id: guard + env: + KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + if [ -z "$KEY" ]; then + echo "::notice::ANTHROPIC_API_KEY not set — MCP maintain skipped (set it to enable)." + echo "ok=false" >> "$GITHUB_OUTPUT" + else + echo "ok=true" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout PR head (writable, same-repo only) + if: steps.guard.outputs.ok == 'true' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + + - name: Skip on last-commit marker + id: marker + if: steps.guard.outputs.ok == 'true' + run: | + if git log -1 --pretty=%B | grep -q '\[skip mcp\]'; then + echo "::notice::last commit marked [skip mcp] — skipping to avoid a self-trigger loop." + echo "run=false" >> "$GITHUB_OUTPUT" + else + echo "run=true" >> "$GITHUB_OUTPUT" + fi + + - name: Run mcp-maintainer + if: steps.guard.outputs.ok == 'true' && steps.marker.outputs.run == 'true' + uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + You are the **mcp-maintainer** for this repo (follow `.claude/agents/mcp-maintainer.md` + exactly — same scope, boundaries, and done-contract). This is an automated CI run on + PR #${{ github.event.pull_request.number }} (head `${{ github.event.pull_request.head.ref }}`). + + TASK: assess whether this PR changed anything that requires the repo's MCP surface to be + built (first time) or updated (drift) — the `.fuze/manifest.json` `mcp` block, the + server(s) under `mcp/` named in `servers[]`, the `mcp/tools.json` tool manifest + (every tool needs name/description/inputSchema/`mutates`), and protocol-version + currency. If the surface is already in sync, DO NOTHING and say so. + + IF a change is needed: + - Make the minimal correct change (scaffold on first run; reconcile only what drifted). + - COMMIT IT BACK TO THIS PR BRANCH: `chore(mcp-maintain): [skip mcp]` and push to + `${{ github.event.pull_request.head.ref }}`. The trailing `[skip mcp]` is REQUIRED so you + do not re-trigger yourself. + - If you cannot push to the PR branch (this is a fork PR), instead open a follow-up PR from + an `mcp-maintain/pr-${{ github.event.pull_request.number }}` branch, add the `auto-merge` + label, and comment the link on this PR. + + HARD BOUNDARIES: never fabricate a tool's product/domain behaviour (scaffold a schema-valid + skeleton and TODO-flag the real behaviour), never handle secrets/creds, never `kubectl`/deploy, + never flip `enabled: true` for a server with no working tool, and never widen a tool's + `mutates: false` to `true` just to make a handler compile — if the handler mutates, the + handler is the bug. Where the repo exposes secret material or infrastructure writes, FLAG the + exposure policy rather than deciding it. Verify any server you touch actually starts and + lists its tools. + + End with the done-contract line from your agent def + (`MCP SURFACE: ` + what landed + `NEEDS PRODUCT/OPERATOR:` if any). + claude_args: '--allowedTools "Bash,Read,Edit,MultiEdit,Write,Glob,Grep,WebFetch"' diff --git a/registration/README.md b/registration/README.md new file mode 100644 index 0000000..72b46f8 --- /dev/null +++ b/registration/README.md @@ -0,0 +1,82 @@ +# FuzeFront registration + +FuzeAgent self-registers with the FuzeFront portal at deploy time. + +| File | Purpose | +|---|---| +| `manifest.json` | App identity, Module-Federation contract, `nav` placement | +| `policy.json` | FuzeAgent's own Permit resources/roles, bare keys | +| `register.sh` | Idempotent registration script from `@fuzefront/onboarding-kit` | + +## This replaces a hardcoded entry in FuzeFront + +FuzeAgent is currently one of only three apps in the portal, and it is registered by a +**hardcoded entry in FuzeFront's `backend/applications/src/app-registry/builtins.ts`** — +i.e. FuzeFront's source has to change for FuzeAgent's portal presence to change. Once +this self-registration is live, that builtin can be retired (deliberately last, and +behind a flag, so the app never vanishes from the menu in the gap). + +## Module Federation is real here + +Unlike most siblings, `integration.type` is genuinely `module-federation`. The +contract in `manifest.json` is copied from `services/ui-react/vite.config.ts`: + +| Field | Value | Source | +|---|---|---| +| `scope` | `fuzeagentApp` | federation `name` | +| `module` | `./FuzeAgentApp` | `exposes` key | +| `remoteEntry` | `https://fuzeagent.prod.fuzefront.com/remoteEntry.js` | `filename` + host | + +React and react-dom are shared singletons, so FuzeFront's React instance is reused — +which is what makes mounting into the host shell work at all. + +## Menu placement + +```jsonc +"nav": { "section": "build", "order": 10 } +``` + +FuzeAgent leads the **build** stage. Menu order was previously unexpressible: the +registry sorted by `created_at`, so the side menu was in registration order. + +## Policy + +Derived from `services/orchestrator/models.py`: `Organization`, `Team`, `Agent`, +`Task`, plus `Goal` (the organizational-goal surface the MCP server exposes). + +`Agent:deploy` and `Task:assign` are separated from ordinary writes deliberately — +they cause an autonomous agent to actually *run*, which is a different kind of +authority from editing a record. + +## MCP is already real + +`.fuze/manifest.json` declares `mcp.enabled: true` — not a scaffold. +`mcp-servers/fuzeagent-server/` already serves 15+ tools against the hierarchy API. + +**Outstanding:** a `tools.json` declaring `mutates` per tool. Several tools mutate and +must be classified `mutates: true`: + +- `assign_task`, `deploy_agent`, `create_custom_agent`, `update_task_status`, + `create_organizational_goal`, `update_goal_progress`, `create_goal_conversation` + +The `list_*` / `get_*` tools are `mutates: false`. `deploy_agent` deserves particular +care: it starts an autonomous agent, so it must never be reachable as a side effect of +a read. Classification is `mcp-maintainer`'s to reconcile. + +## A2A is also real + +`agent-templates/a2a/` implements card generation, the adapter, authz and identity, and +`agent-templates/roles/agent-orchestrator/role.json` is a fully-described serving role. +The manifest names it accurately but leaves `enabled: false`: flipping it requires +verifying the card projects schema-valid against the frozen contract, which is +`a2a-maintainer`'s call rather than a manifest edit's. + +## NOT DONE — init container not wired + +`deploy/helm/fuzeagent/` exists, but this repo is a multi-service deployment and +exactly one deployment must run registration; wiring more than one would have them race +and duplicate-register. Choosing the owner is `devops-engineer`'s call and is flagged +rather than guessed. To finish: paste the init container from +[`@fuzefront/onboarding-kit`](https://github.com/izzywdev/FuzeFront/blob/master/packages/onboarding-kit/helm/initcontainer.yaml) +into that one pod spec, and create the `fuzefront-registration` Secret plus a ConfigMap +of this directory. diff --git a/registration/manifest.json b/registration/manifest.json new file mode 100644 index 0000000..6f5b950 --- /dev/null +++ b/registration/manifest.json @@ -0,0 +1,21 @@ +{ + "manifestVersion": "1", + "slug": "fuzeagent", + "name": "FuzeAgent", + "menuLabel": "Agents", + "description": "AI team orchestration — autonomous agents (Claude Code SDK + CrewAI) organised into teams and coordinated by a digital CEO.", + "icon": { "kind": "emoji", "value": "🤖" }, + "mode": "portal", + "builtin": false, + "integration": { + "type": "module-federation", + "remoteEntry": "https://fuzeagent.prod.fuzefront.com/remoteEntry.js", + "scope": "fuzeagentApp", + "module": "./FuzeAgentApp" + }, + "nav": { "section": "build", "order": 10 }, + "chrome": { "menu": "host", "topbar": "host" }, + "routing": { "path": "/app/fuzeagent" }, + "visibility": "organization", + "roles": [] +} diff --git a/registration/policy.json b/registration/policy.json new file mode 100644 index 0000000..fad612c --- /dev/null +++ b/registration/policy.json @@ -0,0 +1,82 @@ +{ + "name": "FuzeAgent", + "resources": [ + { + "key": "Organization", + "name": "Organization", + "actions": { + "read": { "name": "Read" }, + "create": { "name": "Create" }, + "update": { "name": "Update" }, + "delete": { "name": "Delete" } + } + }, + { + "key": "Team", + "name": "Team", + "actions": { + "read": { "name": "Read" }, + "create": { "name": "Create" }, + "update": { "name": "Update" }, + "delete": { "name": "Delete" } + } + }, + { + "key": "Agent", + "name": "Agent", + "actions": { + "read": { "name": "Read" }, + "create": { "name": "Create" }, + "update": { "name": "Update" }, + "delete": { "name": "Delete" }, + "deploy": { "name": "Deploy" } + } + }, + { + "key": "Task", + "name": "Task", + "actions": { + "read": { "name": "Read" }, + "assign": { "name": "Assign" }, + "update": { "name": "Update" } + } + }, + { + "key": "Goal", + "name": "Organizational Goal", + "actions": { + "read": { "name": "Read" }, + "create": { "name": "Create" }, + "update": { "name": "Update" } + } + } + ], + "roles": [ + { + "key": "viewer", + "name": "Viewer", + "permissions": ["Organization:read", "Team:read", "Agent:read", "Task:read", "Goal:read"] + }, + { + "key": "operator", + "name": "Operator", + "permissions": [ + "Organization:read", "Team:read", + "Agent:read", "Agent:deploy", + "Task:read", "Task:assign", "Task:update", + "Goal:read", "Goal:update" + ] + }, + { + "key": "admin", + "name": "Admin", + "permissions": [ + "Organization:read", "Organization:create", "Organization:update", "Organization:delete", + "Team:read", "Team:create", "Team:update", "Team:delete", + "Agent:read", "Agent:create", "Agent:update", "Agent:delete", "Agent:deploy", + "Task:read", "Task:assign", "Task:update", + "Goal:read", "Goal:create", "Goal:update" + ] + } + ] +} diff --git a/registration/register.sh b/registration/register.sh new file mode 100755 index 0000000..5b767ab --- /dev/null +++ b/registration/register.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env sh +# FuzeFront app self-registration — run as a Kubernetes INIT CONTAINER. +# +# Registers the app with the FuzeFront platform at pod startup: app registry entry, +# AuthZ (Permit) policy, and billing profile. Idempotent — safe to run on every pod +# start, every restart, and concurrently across replicas. +# +# WHY THIS IS AN INIT CONTAINER AND NOT A BEST-EFFORT SIDECAR: +# the app depends on FuzeFront for AuthN, AuthZ, org/user context, and billing. An +# unregistered app cannot function correctly, so a registration failure MUST stop the +# pod — it exits non-zero and the pod CrashLoopBackOffs until the problem is fixed. +# Failing loudly at deploy beats a half-registered app serving traffic. +# +# Required env: +# FUZEFRONT_API_URL base URL, e.g. http://fuzefront-applications:3003 +# FUZEFRONT_REGISTRATION_TOKEN bearer token for a service account with apps:register +# Optional env: +# REGISTRATION_DIR directory holding manifest.json (default: /registration) +# SKIP_ACTIVATE "true" to register but not activate (staged rollout) +# +# Exit codes: 0 = registered/activated (or already was). 1 = anything else. + +set -eu + +REGISTRATION_DIR="${REGISTRATION_DIR:-/registration}" +MANIFEST="${REGISTRATION_DIR}/manifest.json" +POLICY="${REGISTRATION_DIR}/policy.json" +BILLING="${REGISTRATION_DIR}/billing-profile.json" + +# BOTH go to stderr, on purpose. http() returns the HTTP status code on STDOUT and +# is read via command substitution, so anything else written to stdout would be +# captured into the status code and corrupt every comparison against it. Init +# containers send both streams to the pod log, so nothing is lost by this. +log() { echo "[fuzefront-register] $*" >&2; } +die() { echo "[fuzefront-register] FATAL: $*" >&2; exit 1; } + +# ---- preflight --------------------------------------------------------------- +[ -n "${FUZEFRONT_API_URL:-}" ] || die "FUZEFRONT_API_URL is not set" +[ -n "${FUZEFRONT_REGISTRATION_TOKEN:-}" ] || die "FUZEFRONT_REGISTRATION_TOKEN is not set" +[ -f "$MANIFEST" ] || die "no manifest at $MANIFEST" + +command -v curl >/dev/null 2>&1 || die "curl is required but not installed" +command -v jq >/dev/null 2>&1 || die "jq is required but not installed" + +jq empty "$MANIFEST" 2>/dev/null || die "$MANIFEST is not valid JSON" + +SLUG="$(jq -r '.slug // empty' "$MANIFEST")" +[ -n "$SLUG" ] || die "manifest has no .slug" + +# nav placement is what orders the app in the portal's side menu. Not fatal if +# absent (the platform defaults it to the 'platform' section, last) but it almost +# always means someone forgot, so say so loudly rather than silently sorting last. +NAV_SECTION="$(jq -r '.nav.section // empty' "$MANIFEST")" +if [ -z "$NAV_SECTION" ]; then + log "WARNING: manifest declares no .nav.section — this app will sort LAST in the side menu." +fi + +API="${FUZEFRONT_API_URL%/}/api/v1/app-registry" +AUTH="Authorization: Bearer ${FUZEFRONT_REGISTRATION_TOKEN}" + +log "slug=${SLUG} section=${NAV_SECTION:-} api=${API}" + +# ---- helpers ----------------------------------------------------------------- +# Emits the HTTP status on stdout and writes the body to $2. Retries transient +# failures (connection refused / 5xx) — the platform may still be starting up. +http() { + _method="$1"; _url="$2"; _body_file="$3"; _payload="${4:-}" + _attempt=1 + while [ "$_attempt" -le 5 ]; do + if [ -n "$_payload" ]; then + _code="$(curl -sS -o "$_body_file" -w '%{http_code}' \ + -X "$_method" "$_url" \ + -H "$AUTH" -H 'Content-Type: application/json' \ + --data-binary "@$_payload" 2>/dev/null || echo 000)" + else + _code="$(curl -sS -o "$_body_file" -w '%{http_code}' \ + -X "$_method" "$_url" -H "$AUTH" 2>/dev/null || echo 000)" + fi + # 000 = could not connect; 5xx = server-side transient. Both worth retrying. + case "$_code" in + 000|5??) + log " ${_method} ${_url} -> ${_code} (attempt ${_attempt}/5), retrying in $((_attempt * 2))s" + sleep "$((_attempt * 2))" + _attempt=$((_attempt + 1)) + ;; + *) echo "$_code"; return 0 ;; + esac + done + echo "$_code" + return 0 +} + +BODY="$(mktemp)" +# shellcheck disable=SC2064 # expand BODY now, on purpose +trap "rm -f '$BODY'" EXIT + +# ---- 1. register (idempotent) ------------------------------------------------ +CODE="$(http GET "${API}/apps/${SLUG}" "$BODY")" + +case "$CODE" in + 200) + STATUS="$(jq -r '.status // empty' "$BODY")" + log "already registered (status=${STATUS})" + # Re-PUT the manifest so a redeploy picks up manifest changes (new remoteEntry + # after a version bump, changed nav placement, …). Without this, the very first + # registration would be frozen forever and every later manifest edit a no-op. + PUT_CODE="$(http PUT "${API}/apps/${SLUG}" "$BODY" "$MANIFEST")" + case "$PUT_CODE" in + 200|204) log "manifest refreshed" ;; + # A manifest update is not worth failing the pod over — the app IS registered + # and can serve. Report it; the drift shows up in the registry. + *) log "WARNING: manifest refresh returned ${PUT_CODE} — continuing with the existing registration" ;; + esac + ;; + 404) + log "not registered — registering" + REQ="$(mktemp)" + jq '{manifest: .}' "$MANIFEST" > "$REQ" + CODE="$(http POST "${API}/apps" "$BODY" "$REQ")" + rm -f "$REQ" + case "$CODE" in + 201) log "registered" ;; + # Another replica won the race — that is success, not failure. + 409) log "already registered (409 — concurrent replica won the race)" ;; + *) die "register failed: HTTP ${CODE} $(cat "$BODY")" ;; + esac + STATUS="registered" + ;; + 401|403) + die "auth rejected (HTTP ${CODE}) — check FUZEFRONT_REGISTRATION_TOKEN has the apps:register scope" + ;; + *) + die "unexpected response looking up ${SLUG}: HTTP ${CODE} $(cat "$BODY")" + ;; +esac + +# ---- 2. activate ------------------------------------------------------------- +if [ "${SKIP_ACTIVATE:-false}" = "true" ]; then + log "SKIP_ACTIVATE=true — leaving app in '${STATUS}' (it will NOT appear in the menu)" +elif [ "${STATUS:-}" = "activated" ]; then + log "already activated" +else + CODE="$(http POST "${API}/apps/${SLUG}/activate" "$BODY")" + case "$CODE" in + 200|204) log "activated" ;; + *) die "activate failed: HTTP ${CODE} $(cat "$BODY")" ;; + esac +fi + +# ---- 3. AuthZ policy (optional file) ----------------------------------------- +# The product declares its OWN Permit resources/roles with BARE keys; the platform +# namespaces them (_Listing, …) and merges into the base schema. This is +# what replaces hand-editing backend/src/permit/products/*.policy.ts in FuzeFront. +if [ -f "$POLICY" ]; then + jq empty "$POLICY" 2>/dev/null || die "$POLICY is not valid JSON" + CODE="$(http PUT "${API}/apps/${SLUG}/policy" "$BODY" "$POLICY")" + case "$CODE" in + 200|201|204) log "authz policy submitted" ;; + *) die "policy submission failed: HTTP ${CODE} $(cat "$BODY")" ;; + esac +else + log "no policy.json — skipping authz policy (app will have no product-specific roles)" +fi + +# ---- 4. billing profile (optional file) -------------------------------------- +# Registers the product key so billing accepts checkout for it. Replaces editing the +# BILLING_PRODUCT_KEYS env allowlist in the platform's Helm values by hand. +if [ -f "$BILLING" ]; then + jq empty "$BILLING" 2>/dev/null || die "$BILLING is not valid JSON" + CODE="$(http PUT "${API}/apps/${SLUG}/billing-profile" "$BODY" "$BILLING")" + case "$CODE" in + 200|201|204) log "billing profile registered" ;; + *) die "billing profile registration failed: HTTP ${CODE} $(cat "$BODY")" ;; + esac +else + log "no billing-profile.json — skipping billing (app cannot take payments)" +fi + +log "OK — ${SLUG} is registered and ready" +exit 0 From 7090d14d3b03a4b445f34decd499be0e6295c2c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:32:52 +0000 Subject: [PATCH 2/5] fix(governance): supply the mcp-maintainer agent file + auto-merge permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two red checks with the same root shape: something was DECLARED without the file that backs it, and neither gate could self-heal. governance-sync: the manifest declares `mcp-maintainer`, and FuzeSDLC now carries the canonical agent — but governance_sync.py only COPIES a file that already exists and has drifted. A wholly absent one is reported `missing` and hard-fails (governance_sync.py:84), so `--write` was never going to create it. Added .claude/agents/mcp-maintainer.md from the canonical, plus the mcp-maintain.yml workflow where absent, so the declared surface actually has something behind it. auto-merge.yml: no permissions block, so the default read-only GITHUB_TOKEN made `gh pr merge --auto` fail with "Resource not accessible by integration" every run. FuzeSDLC#66 fixed the canonical template; this copy was stale. Workflow drift is deliberately ADVISORY in governance-sync (the job cannot push workflow files), so nothing would ever have reconciled it on its own. Verified with governance_sync.py against the canonical: ok=True, missing=[]. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .claude/agents/backend-engineer.md | 2 +- .claude/agents/frontend-engineer.md | 2 +- .github/workflows/auto-merge.yml | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index dbcc18d..a4bcb03 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 2ccbc44..3e8d6a0 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/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index b5cda93..4783c94 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -6,6 +6,12 @@ on: check_suite: types: [completed] +# The default GITHUB_TOKEN is read-only, so `gh pr merge --auto` fails with +# "Resource not accessible by integration". +permissions: + contents: write + pull-requests: write + jobs: auto-merge: name: Auto Merge PR From 2718bc06384c91a8fe20552cc1c22b53566d8ef2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:34:08 +0000 Subject: [PATCH 3/5] chore(governance): reconcile managed files to FuzeSDLC v1 [skip ci] --- .claude/agents/backend-engineer.md | 2 +- .claude/agents/frontend-engineer.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index a4bcb03..dbcc18d 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, logging, verification-protocol, model-cascade] +skills: [api-contract-first, feature-flags, 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 3e8d6a0..2ccbc44 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, logging, ui-runtime-validation, verification-protocol, model-cascade] +skills: [fuzefront-ui-package, design-system-inheritance, design-system-conformance, ui-frame-contract, frontend-design, feature-flags, ui-runtime-validation, verification-protocol, model-cascade] --- You are a **frontend engineer**. You implement the **UI slice only**. From 81bd18c585f6ab8c55654b935831c515e22e0471 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:35:29 +0000 Subject: [PATCH 4/5] feat(mobile): declare the mobile requirements contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No repo in the family declared `mobile` at all, so `mobile-app-engineer` had nothing to read on merge and no gate could tell "mobile is out of scope" from "nobody wrote it down". Uses the EXISTING canonical contract — FuzeSDLC agent-templates/schema/mobile-requirements.schema.json — which already sanctions a `mobile` block inside .fuze/manifest.json. No new schema and no new agent were invented: mobile-app-engineer, mobile-packaging and mobile-conformance already exist. strategy: pwa matches what FuzeFront actually ships — an installable PWA wrapped as a signed Android TWA — rather than react-native/flutter, which nothing here uses. targets omit ios until there is a signing identity for it. The acceptance list is deliberately concrete (no horizontal scroll at 375, 44px tap targets, Lighthouse >= 80, and the standalone URL rendering with no portal chrome), because an acceptance block of good intentions verifies nothing. Validated against the canonical schema; confirmed a missing `required`, an unknown strategy and an unknown target are all rejected. NOTE ON SEQUENCING: this declares mobile INTENT only. The `modes: ["portal", "standalone"]` + `routing.host` that a mobile build actually wraps cannot be added to registration/manifest.json until FuzeFront#444 merges — AppManifest is additionalProperties:false, so those keys would fail validation against the currently published schema. Registration side follows once that lands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .fuze/manifest.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.fuze/manifest.json b/.fuze/manifest.json index 259e44f..303b80d 100644 --- a/.fuze/manifest.json +++ b/.fuze/manifest.json @@ -79,5 +79,33 @@ "FuzeFront", "FuzeKeys", "FuzePlan" - ] + ], + "mobile": { + "product": "FuzeAgent", + "required": true, + "strategy": "pwa", + "targets": [ + "android", + "mobile-web" + ], + "responsive": { + "min_width": 375, + "breakpoints": [ + 375, + 768, + 1024 + ], + "min_tap_target_px": 44 + }, + "acceptance": [ + "No horizontal scroll at min_width; the shell drawer replaces the desktop sidebar.", + "Every interactive target is at least min_tap_target_px.", + { + "check": "Lighthouse mobile performance", + "min_score": 80, + "at_width": 375 + }, + "The standalone URL loads with no portal chrome \u2014 it is what the APK wraps." + ] + } } From f80e28206ace8c8ade74f8486fac9522897f92a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:16:38 +0000 Subject: [PATCH 5/5] feat(registration): declare standalone surface via modes + routing.host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repo declares a `mobile` block, and a mobile build can only wrap a URL that stands on its own — an app store has nothing to point at otherwise. The manifest previously said only `mode: "portal"`, so there was no standalone surface to wrap and no host to wrap it at. That made the mobile contract unsatisfiable on its face. FuzeFront#444 added `modes` to AppManifest for exactly this: the scalar `mode` is single-valued and so cannot express a product that is genuinely both — mounted in the host shell on the web, and separately reachable on its own host for the mobile wrapper. `modes` is the multi-valued form and wins where both are present. - `modes: ["portal", "standalone"]` — portal stays the default surface. The scalar `mode` is kept unchanged (still REQUIRED, now deprecated) and equals `modes[0]`, as the contract requires. - `routing.host` — the user-facing standalone host, `.fuzefront.com`. Distinct from `*.prod.fuzefront.com`, which is the infra host serving the portal `remoteEntry`. Portal integration is untouched. Validated against packages/onboarding-kit/manifest.schema.json at merged master (9d2b0af). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- registration/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/registration/manifest.json b/registration/manifest.json index 6f5b950..7474c5b 100644 --- a/registration/manifest.json +++ b/registration/manifest.json @@ -6,6 +6,7 @@ "description": "AI team orchestration — autonomous agents (Claude Code SDK + CrewAI) organised into teams and coordinated by a digital CEO.", "icon": { "kind": "emoji", "value": "🤖" }, "mode": "portal", + "modes": ["portal", "standalone"], "builtin": false, "integration": { "type": "module-federation", @@ -15,7 +16,7 @@ }, "nav": { "section": "build", "order": 10 }, "chrome": { "menu": "host", "topbar": "host" }, - "routing": { "path": "/app/fuzeagent" }, + "routing": { "path": "/app/fuzeagent", "host": "fuzeagent.fuzefront.com" }, "visibility": "organization", "roles": [] }