Skip to content
API Reference

API Reference

These are the CORE-side contracts with FORGE. FORGE’s own gRPC API (forge.v1) is FORGE’s to document. It reaches the browser only through the proxy below.

/forge/: the reverse proxy

forgeproxy.go, mux pattern /forge/. A bare /forge redirects to /forge/.

https://<console origin>/forge/<rest>  →  $FORGE_UPSTREAM/<rest>

FORGE serves its bundle and its gRPC-web API at / and is built with --base-href /forge/. Its client therefore calls <origin>/forge/forge.v1.ForgeService/…, and FORGE sees /forge.v1.ForgeService/….

On every request, in this order:

  1. Any inbound X-Core-Identity header is deleted. A browser can never hand FORGE an assertion of its own choosing.
  2. A CORE session is required. With no signed-in email the answer is 401 and nothing is proxied. This holds including the console’s auth-disabled “open” mode, because FORGE acts as a named person on GitHub.
  3. The key must resolve. CORE_FORGE_IDENTITY_KEY is base64 of at least 32 bytes, minted by core-operator’s provisioner as core-system/forge-identity-key and mirrored into forge-system. If it does not resolve, the answer is 503, naming the variable.
  4. CORE signs X-Core-Identity as coreidentity.Sign(key, email, "forge", now). The audience is forge, and an assertion is valid for coreidentity.MaxAge (2 minutes).

On the way out, the proxy also:

  • sets the standard X-Forwarded-* headers and X-Forwarded-Prefix: /forge;
  • sends FORGE’s own Service name as Host;
  • removes the CORE session cookie (core_console_session) from the forwarded Cookie header and keeps any other cookies. FORGE trusts the assertion and nothing else;
  • flushes every write immediately (FlushInterval -1), so gRPC-web server streams reach the browser message by message. grpc-status headers and trailers pass through untouched.
VariableDefaultMeaning
CORE_FORGE_IDENTITY_KEYnone (required)HMAC key shared with FORGE. Fails closed
FORGE_UPSTREAMhttp://forge.forge-system.svc.cluster.local:8080Where FORGE is reached. Must be an http(s) URL with a host

The other half of the security is a NetworkPolicy in grpc/infrastructure/apps/onhost/50-forge.yaml: forge-system admits ingress on 8080 from the core-operator pod only, because the assertion is a bearer credential for its lifetime.

Errors. A browser navigation (Accept: text/html, not gRPC) gets a small HTML page titled “FORGE is not available here”, carrying the message and the status. Every other caller gets {"error": "<message>"}. The messages are listed in Troubleshooting.

Framing. The console sends neither X-Frame-Options nor a CSP frame-ancestors, so the same-origin iframe needs nothing relaxed.

GET /api/forge

forge.go + forgestatus.go. Session-gated and read-only. It lists the org’s forged apps (repositories with topic forged) with their live state, read from GitHub with the console’s App installation token. The org comes from the App installation, not from a console setting.

No console page reads this endpoint since the by-kind pages (Web apps, RIVER pipelines) were removed on 2026-09-26. The FORGE studio reads core/ci itself. An owner decision on removing GET /api/forge is pending.

Response (shape from the handler):

{
  "apps": [
    {
      "name": "…", "description": "…", "html_url": "…", "updated_at": "…",
      "kind": "web | pipeline",
      "status": {
        "open_jobs": 0, "has_brief": true,
        "default_branch": "main", "verification": "core/ci",
        "last_run_found": true, "last_run_status": "success",
        "last_run_url": "…", "last_run_sha": "…", "last_run_description": "…",
        "state": "live", "state_detail": "core/ci green on the default branch",
        "source": "github", "read_at": "…", "cached": false, "reads": {}
      }
    }
  ],
  "org": "…", "source": "github", "complete": true,
  "unmeasured": [], "statusComplete": true
}

kind is pipeline for topic forge-pipeline, and web otherwise. status is absent (not null, not zeroed) when it was not read, and the reason is named in unmeasured[].

State precedence (forgeState):

Conditionstatestate_detail
open forge issues > 0workingN open job(s) for the agent
core/ci = pendingbuildingCORE is verifying the default branch (core/ci)
core/ci = successlivecore/ci green on the default branch
core/ci = failurefailedcore/ci failed on the default branch — see the run
core/ci = errorfailedcore/ci could not run the checks — see the run
no core/ciidleCORE has not verified the default branch head yet

live only ever means CORE’s verification is green. Nothing deploys a forged app yet.

Cost bounds. The endpoint makes at most three GitHub reads per app. It enriches at most the 12 most recently updated apps per request, caches each for 60 s, uses 4 workers, and walks at most 10 pages of 100 repositories (truncated otherwise). It skips uncached reads when the GitHub quota is at or below the reserve kept for the agent fleet.

Methods other than GET answer 405 with Allow: GET and the body “CORE no longer files briefs — open FORGE › Studio and send it there”.

Deleted: the console’s brief endpoints

POST /api/forge, POST /api/forge/change and POST /api/forge/plan were a second repo-create and issue-file path, plus a CORE-side planner. They bypassed FORGE’s app kinds, CI skeletons, audit log and planner, and were deleted with their tests. TestTheConsoleFilesNoBriefs fails if one comes back. FORGE is the only producer of forge issues.

Studio link parameters

The iframe src the console builds (forge_command.dart). The contract belongs to FORGE (its link_prefill.dart). Values are percent-encoded, spaces as %20, and appear in a fixed order: chat, app, panel, cmd.

ParamMeaning
chatThe chat (= project) to open: a FORGE chat id (c- + 16 hex, or repo-<app name>), or new for an empty chat. View state: FORGE keeps it, so a reload shows the same chat
panelThe side panel to open: components, draft or details. View state
appLegacy, from before chats: an app name (^[a-z0-9][a-z0-9-]{0,99}$), which FORGE opens as the one-lane chat repo-<name>. Kept so old links still work. CORE sends chat now
cmdA command FORGE prefills into its console and never sends. FORGE strips it from its address after reading it, and caps it at 8000 UTF-8 bytes. No CORE page builds one today

Console deep links: /?tab=forgeStudio&chat=<id> (and the legacy &app=<name>&panel=<panel>).

The forge.chat message

FORGE tells CORE which chat it is showing with a same-origin postMessage from the studio frame. The data is JSON text:

{"type": "forge.chat", "id": "c-0123456789abcdef", "title": "…", "project": "…"}

CORE accepts it only when all of these hold (acceptStudioMessage):

  • event.origin equals CORE’s own origin;
  • event.source is the studio iframe’s own window;
  • the data is a JSON string with type forge.chat and an id of FORGE’s chat-id shape.

Title and project are trimmed and capped at 200 characters. The message carries ids and display text only. It sets the Studio’s page header and moves the rail’s selection. It is never a command.