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:
- Any inbound
X-Core-Identityheader is deleted. A browser can never hand FORGE an assertion of its own choosing. - A CORE session is required. With no signed-in email the answer is
401and nothing is proxied. This holds including the console’s auth-disabled “open” mode, because FORGE acts as a named person on GitHub. - The key must resolve.
CORE_FORGE_IDENTITY_KEYis base64 of at least 32 bytes, minted by core-operator’s provisioner ascore-system/forge-identity-keyand mirrored intoforge-system. If it does not resolve, the answer is503, naming the variable. - CORE signs
X-Core-Identityascoreidentity.Sign(key, email, "forge", now). The audience isforge, and an assertion is valid forcoreidentity.MaxAge(2 minutes).
On the way out, the proxy also:
- sets the standard
X-Forwarded-*headers andX-Forwarded-Prefix: /forge; - sends FORGE’s own Service name as
Host; - removes the CORE session cookie (
core_console_session) from the forwardedCookieheader 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-statusheaders and trailers pass through untouched.
| Variable | Default | Meaning |
|---|---|---|
CORE_FORGE_IDENTITY_KEY | none (required) | HMAC key shared with FORGE. Fails closed |
FORGE_UPSTREAM | http://forge.forge-system.svc.cluster.local:8080 | Where 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.
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):
| Condition | state | state_detail |
|---|---|---|
open forge issues > 0 | working | N open job(s) for the agent |
core/ci = pending | building | CORE is verifying the default branch (core/ci) |
core/ci = success | live | core/ci green on the default branch |
core/ci = failure | failed | core/ci failed on the default branch — see the run |
core/ci = error | failed | core/ci could not run the checks — see the run |
no core/ci | idle | CORE 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.
| Param | Meaning |
|---|---|
chat | The 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 |
panel | The side panel to open: components, draft or details. View state |
app | Legacy, 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 |
cmd | A 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.originequals CORE’s own origin;event.sourceis the studio iframe’s own window;- the data is a JSON string with
typeforge.chatand anidof 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.