---
name: fah-creator
description: Manage Fah games through the Creator API using a human-issued scoped API key. Use for game creation, draft edits, version registration, release status, publishing, rollback and unpublishing. Uploads immutable release archives. Does not approve validation, issue credentials or administer members.
---

# Fah Creator

This public skill is served directly at `/creator/v1/skill/SKILL.md` on the Fah host. No repository access or sign-in is needed to read it. Download this file and the [Python client](scripts/creator.py) into a local skill directory, preserving the `scripts/creator.py` path. Resolve that relative download link against the skill URL. Use Python 3.10 or later; no third-party packages are required.

For building the game runtime, use the public [Fah developer docs](https://docs.fah.portal-interactive.com/) and [SDK getting-started guide](https://docs.fah.portal-interactive.com/getting-started.html). Those docs cover lifecycle, input, audio, graphics and host integration. This skill covers Backend game records and release operations; the human manages account access in Creator Portal.

Use the human's `FAH_CREATOR_ORIGIN` and `FAH_CREATOR_API_KEY` environment variables. The origin is the exact HTTPS Fah host, with no path or explicit port. Never ask for the secret in chat or put it in command arguments, generated files, logs or commits. The human creates/revokes keys in `/creator`; a key cannot manage credentials, memberships or ownership. Keep activity within the user's requested games and actions. Existing explicit authorization remains sufficient; this skill grants none by itself.

The bundled standard-library client reads credentials from the environment, refuses redirects, limits writes to game operations and persists an immutable request before sending. It never writes the credential to its operation file. Use an ignored directory outside the source checkout for operation files; retain them until uncertain results are resolved. Each new intended change needs a new file/key. Use the script path relative to this skill directory.

```sh
python3 scripts/creator.py read /creator/v1/me
python3 scripts/creator.py read '/creator/v1/games?limit=50'
```

Check `/creator/v1/me` for game IDs, scopes and expiry, then read the game's current role/state. Effective permission is the intersection of key scope and current account role. Read-only keys cannot change games. `gameIds` contains exact IDs, trailing-star prefixes such as `studio-*`, or `*` for all games the account can manage, including future games. A new game ID must match one of these patterns; choose its permanent ID from the user’s request instead of requiring it during key setup. Wildcards never bypass membership or role checks. Follow each `nextAfter` cursor until null; versions sort by UUID, not date. Compare `createdAt` to determine older versions.

## Drafts and versions

- Create: `POST /creator/v1/games`, body `{"id":"permanent-game-id","metadata":{...}}`; requires `games:create`.
- Read/edit: `GET`/`PATCH /creator/v1/games/{id}`. PATCH requires `games:write`, owner/editor role and the exact GET response ETag. Send `{"metadata":{...}}` with the **complete** draft, preserving untouched fields. Draft edits do not change the public version.
- Versions: `GET`/`POST /creator/v1/games/{id}/versions`. POST requires `versions:write` and owner/editor role. Body fields: canonical UUID v4 `versionId`, 40-hex `sourceCommit`, 64-hex `manifestDigest` and `artifactDigest`, `entryPoint:"g/{id}/index.html"`, semantic `sdkVersion`, `protocolVersion:2`, `manifestVersion:2`, and complete `metadata`. Hash actual artifact/manifest bytes; do not invent digests or imply uploads occurred.

Required metadata: `title` (1–120), `genre`/`style`/`camera` (1–32 lowercase letters/hyphens), `colors:{a,b}` (six-digit hex colors). Optional: `tagline` (240), `author:{name,url}` (100/512, HTTPS URL without credentials), `colors.ink`, `input:{scheme,hint:{glyph,text},consumes}`, `preview:{poster,loop,first,seconds}`, `boot`. Schemes: tap/hold/drag/swipe/aim/stick; hint glyph lowercase letters/hyphens ≤32 and text ≤28; consumes is `[]` or `["swipeY"]`. Preview paths are ASCII relative paths ≤256, no empty/dot segments, encoding, query or fragment; poster is required if preview exists; seconds is >0 and ≤60. Boot is cooperative/blocking. Unknown fields are rejected; mutation bodies are limited to 16 KiB.

## Upload a release

Register the version, then upload its exact ZIP bytes to `PUT /creator/v1/games/{id}/versions/{versionId}/artifact` with `Content-Type: application/zip`, `X-Fah: 1` and an idempotency key. This requires `versions:write` and owner/editor role. The ZIP SHA-256 must equal the registered `artifactDigest`. Prepare a retryable upload using the bundled client:

```sh
python3 scripts/creator.py prepare-upload /creator/v1/games/my-game/versions/VERSION_UUID/artifact \
  --artifact-file game.zip --operation-file /tmp/fah-upload.json
python3 scripts/creator.py send /tmp/fah-upload.json
python3 scripts/creator.py read /creator/v1/games/my-game/versions
```

Replace `VERSION_UUID` with the registered UUID. Keep the archive unchanged while retrying. The operation file records its absolute path and SHA-256, never the key. `202` means the bytes are accepted into quarantine; it does not mean the game is published.

The archive contains files under `g/{id}/`, `kit/` and `lib/`, including `g/{id}/index.html`, `g/{id}/game.json` and the approved `kit/portal-kit.js`. Use relative URLs so the whole bundle runs under `/releases/{versionId}/`. The manifest must identify `manifest:2`, the same `id`, `kit` version, `source.commit` and registered card metadata. Include every referenced preview file. See the [publishing guide](https://docs.fah.portal-interactive.com/creator.html) for packaging and supported SDK builds.

Limits: 16 MiB ZIP, 64 MiB expanded, 1,000 regular files, 128 MiB queued bytes per uploader. ZIP entries must use stored or deflated compression and ASCII relative paths, without hidden files, duplicate paths, symlinks, dot segments or encryption. Do not include credentials, source environment files or unrelated games. No directory-only entries are needed.

Registration starts `pending`. Upload state is `missing`, `queued`, `checking`, `delivered` or `rejected`. A separate trusted worker checks the archive and manifest digests, paths, metadata, approved SDK bytes and immutable object read-back. Only this worker can make validation `ready` or `rejected`. `ready` describes bundle integrity and delivery, not a certification of gameplay or save compatibility; run browser/game tests before publishing. The worker never executes submitted code with its storage credentials.

Read versions after upload. `upload.detail` explains a rejection such as `manifest-digest`, `manifest-metadata-title`, `unsupported-sdk-build` or `missing-preview-poster`. Fix the build and register a new UUID; rejected versions are immutable. `delivery-unavailable` remains pending and retries with backoff. Poll at most every five seconds for two minutes per invocation, then report pending state and resume later; never bypass the publication gate.

## Publish, roll back or unpublish

Read `/creator/v1/games/{id}/publication` and retain its ETag. Select a `ready` version for that game. Publishing and rollback use the version's immutable metadata, not today's draft. Preserve the user's intended version; confirm save compatibility with available build evidence before a rollback, since the API does not migrate player saves. If required evidence is missing, report it to the user before changing publication.

Prepare a body file containing `{"versionId":"ready-version-uuid"}` (or `{"versionId":null}` to unpublish), then prepare and send one operation. These actions require `publication:write` and owner/publisher role.

```sh
python3 scripts/creator.py prepare PUT /creator/v1/games/my-game/publication \
  --body-file publication.json --if-match '"publication-3"' --operation-file /tmp/fah-publish.json
python3 scripts/creator.py send /tmp/fah-publish.json
python3 scripts/creator.py read /creator/v1/games/my-game/publication
```

For draft creation/editing or version registration, use the same prepare/send flow with the appropriate method/path/body and draft ETag where required. Review the prepared file's origin, path, body and revision before sending within the user's authorization.

## Recovery and stopping

- Timeout, connection loss, 408, 429 or 5xx: the commit may already exist. Retry `send` using the **same operation file**, after resolving connectivity or respecting `Retry-After`. Never generate another key or edit the file to recover an uncertain result. Attempt at most one recovery retry per invocation; if still uncertain, retain the file and report the operation ID and ambiguity.
- Success or replay: GET current state. An old success receipt does not describe today's publication and must never trigger a new write to restore it.
- 412: read latest state, reconcile with user intent, then prepare a **new** operation. Do not overwrite another editor's work silently.
- 401/403: stop and report expired/revoked credentials, scope or role constraints. Do not fall back to cookies or ask an agent to issue a broader key.
- 409 `operation-conflict` or `version-not-ready`: stop and resolve the conflict/validation state; do not bypass the gate.

Unpublishing removes discovery, not existing immutable URLs or cached bytes. Report API state and test evidence separately from actual delivery or game playability.
