Fork Horse repository, listed by its owner. Publisher snapshot.
docs/core-integration.md
Snapshot at 34e3175
1# Public core integration
2
3FH-012 composes the MCP OAuth adapter, owner dashboard and project API in one core Worker. The pinned OS still has its own router and is unmodified. `brand.json` remains the display-name authority. The project's owner-operated instance is live: FH-012L verified core and push activity, FH-018 verified automatic fork subscriptions, and the board records an OAuth tool call, the Kumo sidebar and owner promotions. See the [dated acceptance table](../README.md#live-versus-roadmap); each new owner's instance still needs its own verification.
4
5## Owner configuration
6
7Copy the tracked template to gitignored `deployment.local.jsonc` and set `core.enabled: true` only after the deployment has been reviewed. The core block specifies:
8
9- `workerName`: permanent core identity and its Supervisor storage. Renaming it does not migrate projects.
10- `hostname`: a custom hostname distinct from the OS router. It becomes `CORE_PUBLIC_ORIGIN`; workers.dev and preview URLs are disabled.
11- `mcpPath`: a single path segment, normally `/mcp`, distinct from reserved owner and OAuth routes.
12- `project`: the existing project used for MCP authentication/enrollment and initially offered by the dashboard. Create it with the owner API before connecting MCP clients.
13- `artifactsNamespace`: the namespace for canonical repositories, forks and push events.
14- `oauthKvNamespaceId`: an existing dedicated OAuth KV namespace, or `null` for Wrangler provisioning. Treat grant records as credentials. Do not share this namespace across deployments.
15- `pushQueueId`: the resolved 32-hex ID of `pushQueue`, supplied by the owner before provisioning the event-only token. Core uses this ID directly and needs no Queue Read permission. The owner must verify it belongs to the named consumer Queue in this account.
16- `secretsFile`: optional repository-relative path to an ignored JSON file containing exactly `OWNER_API_TOKEN_HASH` and `CF_EVENTS_API_TOKEN`; `null` uses secrets already installed on the Worker. See first-deploy handling below.
17- `pushQueue` and `deadLetterQueue`: distinct, owner-approved Queue names. Configure both before a live deploy and monitor dead-letter messages.
18- `access.issuer`, `access.audience`, `access.owners`: the core Access application issuer, exact audience, and nonempty owner email allowlist. These are separate from the OS application's Access configuration.
19
20Actual account IDs, hostnames, audiences, email addresses and resource IDs belong only in the gitignored file. `OWNER_API_TOKEN_HASH` is a Worker secret containing the SHA-256 hash of a separately held high-entropy owner credential. Neither the raw credential nor its hash goes in tracked configuration, browser code, MCP grants or a response. Enrolled agent API credentials continue to work on the project API without browser cookies.
21
22## Access and routing
23
24The owner must configure an Access application covering `/app`, `/app/*`, and `/authorize` on the core hostname, with a narrow owner policy. Leave `/projects` and `/projects/*` reachable by non-browser bearer clients. Browser owner requests there carry the same application's `CF_Authorization` cookie, whose JWT the Worker verifies identically to the Access assertion header. Verify that the application cookie reaches these paths during the separately authorized live gate. Do not place interactive Access login in front of the MCP endpoint, OAuth registration/token endpoints, or discovery endpoints: their OAuth challenge and token flows must reach the Worker.
25
26The Worker verifies `Cf-Access-Jwt-Assertion` (or the Access application cookie when no assertion header is present) with RS256 keys from the configured team's `/cdn-cgi/access/certs`, exact issuer/audience, expiry/not-before/issued-at, and the owner email allowlist. A header's presence alone grants nothing. A missing, forged, expired or non-owner assertion is refused. Browser API writes additionally require the configured origin and JSON content type. Cross-origin browser calls cannot use an owner bearer credential to bypass this check.
27
28All dashboard requests, including JavaScript and CSS, pass through the Worker first (`assets.run_worker_first: true`). The Worker strips the `/app` prefix only after authentication and returns private, non-cacheable responses. Root asset aliases and missing assets are not an SPA fallback. Only inert project-selection JSON is injected; no credential is sent to the dashboard. The existing dashboard keeps using same-origin cookies.
29
30MCP discovery, registration, consent, token exchange and agent tools use the FH-004 OAuth provider. The core adapter authenticates against stored enrolled-agent hashes and refuses owner credentials as agent credentials. Approved Access-authenticated consent enrolls a new agent in the configured project. Rotating that agent's credential invalidates its OAuth grant on the next MCP call. Private Supervisor methods carry trusted identity and owner requests over the binding; no public identity header or auth-bypass route is added.
31
32References: [Access JWT validation](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/), [Worker-first static asset routing](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/).
33
34## Per-repository push subscriptions
35
36FH-018 creates a subscription after registering a canonical repository or each new agent fork. The request matches installed Wrangler 4.149.0's `src/queues/client.ts` and `src/queues/cli/commands/subscription/create.ts`: `POST /accounts/{account}/event_subscriptions/subscriptions` with `name`, `enabled: true`, `source: {type: "artifacts.repo", namespace, repo_name}`, `events: ["pushed"]`, and `destination: {type: "queues.queue", queue_id}`. The delivered event is `cf.artifacts.repo.pushed`; the account-level `artifacts` source has no `pushed` event.
37
38The owner supplies `CF_EVENTS_API_TOKEN` as a Worker secret: an owner-created token restricted to the deployment account and only the event-subscription permissions required for list/create. Exact provider permission availability and provisioning remain owner/live gates. Core never resolves Queue names with this token or uses it for Artifacts Git operations. `CF_ACCOUNT_ID` comes from the existing top-level `accountId`; `PUSH_QUEUE_ID` comes from `core.pushQueueId`. No token is stored in a var, returned, or logged.
39
40Core lists subscriptions using Wrangler's `queue_id`, `page`, and `per_page` query parameters before creating. It adopts an enabled matching source, repository, destination and `pushed` event, including one created by an earlier attempt whose response was lost. It stops safely on an incomplete/failed listing. Scans are bounded to 100 pages of 20 subscriptions and 10 seconds per attempt; reaching either bound leaves pending state for owner inspection/retry. Concurrent changes by external administrators are outside the per-project serialization guarantee.
41
42Registration atomically saves `repoEvents:<repo>` beside the membership registry and arms an alarm before external API work. API failure does not fail create/join: responses, `/state`'s project/forks, and `/me`'s project/caller-filtered forks show `events: "pending"`. Successful entries show `events: "active"` and their persisted `subscriptionId`. This means subscription provisioning succeeded, not that any push was delivered. Attempts back off from 30 seconds to one hour. Each alarm handles up to ten due repositories, shares the Supervisor serialization gate, and re-arms remaining work; it requires no new user request. Provider errors and response bodies are discarded. Manual redirects are rejected without forwarding the bearer token.
43
44Pre-FH-018 repositories without subscription records are shown as pending but are not automatically migrated; owner inventory/backfill is a follow-up live task. Active subscriptions are not periodically revalidated after external deletion or configuration changes. Inventory those cases during FH-012L. The consumer continues to reject other namespaces and unregistered repositories and retries incomplete diffs. Live acceptance must demonstrate canonical and fork push delivery, duplicate handling and dead-letter behavior.
45
46## First-deploy secrets
47
48A new Worker needs both required secrets in its first `wrangler deploy`; `wrangler secret put` cannot provision a nonexistent Worker. Set `core.secretsFile` locally, for example to `packages/forkhorse-core/.wrangler/deploy-secrets.json` (already ignored). The owner file is a JSON object with the lowercase 64-hex `OWNER_API_TOKEN_HASH` and nonempty `CF_EVENTS_API_TOKEN`. Do not commit it. The owner resolves the Queue ID and provisions credentials separately; this packet does not do either live.
49
50`pnpm deploy` validates the file is inside this repository, ignored and untracked, and not a symlink. It makes a private, exclusively created temporary copy in the ignored core `.wrangler` directory, passes `--secrets-file` to only the core deploy, suppresses that subprocess's output and disk log, and removes the exact copy after success or failure. The owner source file remains owner-managed. A changed temporary file causes cleanup to refuse deletion. The helper never prints parser/provider errors or secret contents. `pnpm check` uses only `--dry-run`, never reads the secret file and needs no real secret. With `secretsFile: null`, an existing Worker's installed secrets are used; first deploy then fails if they do not exist.
51
52## Checks and remaining gates
53
54Run root and upstream frozen installs, the core package's test/typecheck/build, `pnpm test`, `pnpm lint`, and owner-configured `pnpm check`. Check builds MCP and the production dashboard before the core's Wrangler dry run. Generated configurations are temporary and removed after the check. The tracked template intentionally fails placeholder validation: no owner configuration means plain `pnpm check` is blocked, not passed.
55
56For the explicitly approved FH-012/FH-018 offline acceptance only, an isolated worktree may use a synthetic, gitignored `deployment.local.jsonc` created exclusively from the public template. Use an all-zero account ID, `*.fixture.idiot.tax` hosts, a fictional admin address at `idiot.tax`, fixture Access values and `core.enabled=true`. Run with a fresh temporary HOME, an empty inherited environment except PATH, and external networking denied (loopback is allowed for workerd). Use the already-installed pinned tools; do not bootstrap tools, log in, use Keychain or copy any owner's configuration. Refuse to start if the local file exists, and remove only the exact synthetic file created by that invocation on completion or failure. Report tests, builds and all configured Worker dry runs as offline evidence. The owner-configured check at the reviewed SHA remains a separate pre-deploy requirement.
57
58Workerd integration tests use newly generated test signing keys, intercepted JWKS, local KV/DOs, built production dashboard assets and fixture Artifacts. They exercise missing/invalid JWTs, owner API authorization and origin checks, MCP tools/list, PKCE consent/enrollment, and grant invalidation after rotation. Core tests build the dashboard first and serve that output through a local assets binding; this is not evidence of deployed UI acceptance.
59
60For a new owner's instance, live DNS/TLS, Access policy/cookies, provider credentials, OAuth clients, Artifacts operations, Queue/subscription delivery and the custom-domain deployment require separate operational acceptance. FH-032 is building runner integration and default receipt enforcement offline; live receipts await FH-032L. FH-013 supplies [existing-fork token issuance and authenticated-actor discovery](../packages/forkhorse-core/README.md#caller-identity-and-existing-fork-credentials-fh-013); refreshing a token does not require another join.
61