Skip to content

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.

ServicePackageStatus
IdentityServiceforge.v1Current
ProjectServiceforge.v1Current: the canvas and the approval
ChatServicerunink.ui.chat.v1Current: the transcript, org-runink/ui’s shared contract
ForgeServiceforge.v1Current: the planner and single-app reads. CreateApp and RequestChange are the pre-project path
ChatServiceforge.v1Deprecated alias, kept for one release

IdentityService

RPCRequest → ResponseBehaviour
WhoAmIWhoAmIRequest → 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.

RPCRequest → ResponseBehaviour
GetProjectGetProjectRequest{chat_id, fresh} → ProjectThe 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.
SetDraftSetDraftRequest{chat_id, lanes, title, project} → ProjectReplaces 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.
ApproveChatApproveChatRequest{chat_id, briefs[]} → stream ChatEventThe 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_id names 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

MessageFields
Projectsummary (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)
ChatSummaryid, title, project (machine name), repo_url (empty until the first approval), created_at, updated_at, lane_count, imported
Laneid, name, kind (web or pipeline), path, components[], committed, status
LaneComponentid (a catalog id), connection (a CORE connection id, never a credential), settings (non-secret map)
LaneStatusopen_jobs, closed_jobs, verification_state (pending, success, failure, error, or empty), verification_description, verification_url, issues[]
LaneIssuenumber, title, state (open or closed), url, created_at, brief (verbatim)
LaneBrieflane_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.

RPCBehaviour
ListChatsThe 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.
GetChatOne chat’s transcript
CreateChatStores an empty chat titled “New project” by default. Creates no repo.
AppendMessagesAdds 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

RPCRequest → ResponseBehaviour
ProposePlanProposePlanRequest{message, app} → PlanTicket{plan_id}Hands free-form text to the sovereign planner and returns at once. See The planner.
GetPlanGetPlanRequest{plan_id} → Planstatus: thinking, ready or failed. Also error (the inference plane’s own text), reply, and steps[]. NotFound after a restart or expiry.
ListAppsListAppsRequest → ListAppsResponse{apps[]}Every repo with the forged topic
GetAppGetAppRequest{name} → AppOne app’s live state (see below)
ListIterationsListIterationsRequest{name} → ListIterationsResponse{iterations[]}One iteration per forge issue, oldest first, each paired by time with core/ci. See GitHub is the database.
CreateAppCreateAppRequest{name, description, kind} → stream ForgeEventPre-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.
RequestChangeRequestChangeRequest{name, instruction} → stream ForgeEventPre-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:

StateWhenstate_detail
workingThere are open agent jobs (these always win)N open job(s) for the agent
buildingcore/ci is pendingCORE is verifying (core/ci)
livecore/ci is successlast verification green (core/ci). Not serving: deploy is not wired
failedcore/ci is failure or errorlast 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
idleNo 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

CodeTypical cause
UnauthenticatedNo valid CORE identity: FORGE is opened from CORE — sign in to CORE
PermissionDeniedNot on FORGE_OWNER_EMAIL
FailedPreconditionA missing configuration (identity key, GitHub App, appfs, inference), or a state rule such as “name the project first”
InvalidArgumentA validation rule: names, lanes, brief length, components, settings
NotFoundNo such chat, forged repo or plan
ResourceExhaustedThe planner is already decoding a plan
UnavailableGitHub 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.