gRPC API
FORGE has no REST API. It serves gRPC and gRPC-web on one port, from the same
origin as its Flutter bundle. Behind CORE, the browser calls
https://core.runink.org/forge/<package>.<Service>/<Method>. CORE strips /forge and
proxies the call, adding the identity assertion. Every RPC except gRPC reflection passes
the identity check. Without a valid assertion the
answer is Unauthenticated.
| Service | Package | Status |
|---|---|---|
IdentityService | forge.v1 | Current |
ProjectService | forge.v1 | Current: the canvas and the approval |
ChatService | runink.ui.chat.v1 | Current: the transcript, org-runink/ui’s shared contract |
ForgeService | forge.v1 | Current: the planner and single-app reads. CreateApp and RequestChange are the pre-project path |
ChatService | forge.v1 | Deprecated alias, kept for one release |
IdentityService
| RPC | Request → Response | Behaviour |
|---|---|---|
WhoAmI | WhoAmIRequest → WhoAmIResponse{user} | Returns the address CORE vouched for. There is no login or logout RPC: both belong to CORE. |
User: id, email, display_name.
ProjectService
What a chat is about: its lanes and the approval.
| RPC | Request → Response | Behaviour |
|---|---|---|
GetProject | GetProjectRequest{chat_id, fresh} → Project | The summary, the lanes (the draft merged with forge.yaml) and each lane’s live status from GitHub. No transcript. fresh bypasses the 10-second shared read. |
SetDraft | SetDraftRequest{chat_id, lanes, title, project} → Project | Replaces the draft structure. Validated, and files nothing. status and committed are ignored on input. An empty title or project keeps the current value. Changing project is refused once the repo exists. |
ApproveChat | ApproveChatRequest{chat_id, briefs[]} → stream ChatEvent | The only write to GitHub. The first approval creates the repo. Later approvals sync forge.yaml. Then one brief is filed per lane. Each GitHub write is audited first. The stream ends at hand-off. |
ApproveChat preconditions (the error text is verbatim):
- the chat has at least one lane:
add a lane first — a project is its lanes; - each
LaneBrief.lane_idnames a lane in the chat (no lane "…" in this project), with one brief per lane (one brief per lane: "…" has two); - each brief is at least 12 characters,
lane "…": describe the change in at least a sentence — it is handed to the coding agent verbatim, and at most 8000:lane "…": brief too long (8000 chars max); - on the first approval: a valid project name,
name the project first (the project needs a short machine name: lowercase letters, digits, hyphens — it becomes the repo), and at least one brief,the first approval needs at least one lane brief — it is what the coding agent builds.
ChatEvent: stage, message, url, done, failed. The stages the server emits
are repo, manifest, dispatch, failed and done. The events are also appended to
the chat’s transcript with the role system. The final event reads: “Handed off. The
coding agent works each lane’s brief on the box’s own coder model — minutes, not
seconds; the canvas re-reads GitHub.”
Project messages
| Message | Fields |
|---|---|
Project | summary (ChatSummary), lanes[], status_error (why the live status could not be read, or empty), status_read_at (when the oldest GitHub answer used was read) |
ChatSummary | id, title, project (machine name), repo_url (empty until the first approval), created_at, updated_at, lane_count, imported |
Lane | id, name, kind (web or pipeline), path, components[], committed, status |
LaneComponent | id (a catalog id), connection (a CORE connection id, never a credential), settings (non-secret map) |
LaneStatus | open_jobs, closed_jobs, verification_state (pending, success, failure, error, or empty), verification_description, verification_url, issues[] |
LaneIssue | number, title, state (open or closed), url, created_at, brief (verbatim) |
LaneBrief | lane_id, text (sent verbatim) |
runink.ui.chat.v1.ChatService
The transcript half of a chat, served through org-runink/ui’s shared chat handler over FORGE’s own store. The chat id is the project id.
| RPC | Behaviour |
|---|---|
ListChats | The caller’s chats, newest first (default 50), plus every forged repo no chat of theirs owns yet, as an imported one-lane chat repo-<name>. When GitHub cannot be listed, the stored chats still come back. |
GetChat | One chat’s transcript |
CreateChat | Stores an empty chat titled “New project” by default. Creates no repo. |
AppendMessages | Adds 1 to 50 transcript entries. Roles are user, forge, planner and system. A chat holds at most 400 messages of at most 8000 bytes each. Never files anything. |
Titles are at most 120 characters, and a person keeps at most 500 chats. Another person’s chats are never listed or read.
ForgeService
| RPC | Request → Response | Behaviour |
|---|---|---|
ProposePlan | ProposePlanRequest{message, app} → PlanTicket{plan_id} | Hands free-form text to the sovereign planner and returns at once. See The planner. |
GetPlan | GetPlanRequest{plan_id} → Plan | status: thinking, ready or failed. Also error (the inference plane’s own text), reply, and steps[]. NotFound after a restart or expiry. |
ListApps | ListAppsRequest → ListAppsResponse{apps[]} | Every repo with the forged topic |
GetApp | GetAppRequest{name} → App | One app’s live state (see below) |
ListIterations | ListIterationsRequest{name} → ListIterationsResponse{iterations[]} | One iteration per forge issue, oldest first, each paired by time with core/ci. See GitHub is the database. |
CreateApp | CreateAppRequest{name, description, kind} → stream ForgeEvent | Pre-project path: one repo, one kind, one brief. The name must match ^[a-z][a-z0-9-]{1,38}$, and the description (the brief) must be 12 to 8000 characters. Ends at hand-off. |
RequestChange | RequestChangeRequest{name, instruction} → stream ForgeEvent | Pre-project path: files a change on an existing app. The instruction must be 8 to 8000 characters. |
ProposedStep: action (new or change), app, kind (for new), brief
(verbatim if approved as-is), rationale (shown to you, never sent to the agent), and
judgement (runink.ui.judgement.v1.Judgement, unset when judging is off).
App.state is derived in this order:
| State | When | state_detail |
|---|---|---|
working | There are open agent jobs (these always win) | N open job(s) for the agent |
building | core/ci is pending | CORE is verifying (core/ci) |
live | core/ci is success | last verification green (core/ci). Not serving: deploy is not wired |
failed | core/ci is failure or error | last verification failed (core/ci) — see the run, or CORE could not run the verification (core/ci error) — see the run, followed by CORE’s own description |
idle | No core/ci | — |
App also carries repo_url, app_url (always empty: no route exists), updated_at,
open_jobs, kind, last_run_url (the CORE run), and the raw verification_state,
verification_description and verified_sha.
ForgeEvent: stage (validating, repo, skeleton, dispatch, done or
failed), message, url, done, failed.
forge.v1.ChatService (deprecated)
ListChats, GetChat, CreateChat, AppendMessages, SetDraft and ApproveChat,
answering exactly as the two services above do, over the same store. The service is kept
only so that clients built before the split keep working. It is then to be removed. New
clients use runink.ui.chat.v1.ChatService together with forge.v1.ProjectService.
Error codes
| Code | Typical cause |
|---|---|
Unauthenticated | No valid CORE identity: FORGE is opened from CORE — sign in to CORE |
PermissionDenied | Not on FORGE_OWNER_EMAIL |
FailedPrecondition | A missing configuration (identity key, GitHub App, appfs, inference), or a state rule such as “name the project first” |
InvalidArgument | A validation rule: names, lanes, brief length, components, settings |
NotFound | No such chat, forged repo or plan |
ResourceExhausted | The planner is already decoding a plan |
Unavailable | GitHub or the store could not be read or written, or the audit entry could not be written (so the action was not sent) |
Each message is spelled out on the Troubleshooting page.
Sources
grpc/api/proto/forge/v1/forge.proto; grpc/api/proto/forge/v1/chat.proto;
grpc/cmd/{serve,identity_server,project_server,chat_server,chat_ui,forge_server,plan_server,iterations_server}.go.