Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-mcp/src/core.ts
Snapshot at 34e3175
1/** Verified core identity. The FH-012 adapter must reject owner credentials. */
2export interface AgentIdentity {
3 project: string;
4 agent: string;
5}
6/** A live, immutable D1 binding returned by core. */
7export interface ClaimBinding {
8 packet: string;
9 agent: string;
10 repoName: string;
11}
12export interface StateSnapshot {
13 claims: ClaimBinding[];
14 packets?: { id: string; mode?: "standard" | "race"; [key: string]: unknown }[];
15 races?: import("./races").RaceState[];
16 [key: string]: unknown;
17}
18export interface WorkFile {
19 path: string;
20 content: string;
21}
22/** Owner identity supplied only after FH-012 verifies its Access JWT and owner policy. */
23export interface OwnerIdentity {
24 subject: string;
25}
26export interface EnrolledAgent extends AgentIdentity {
27 apiToken: string;
28}
29
30export type CoreOperation =
31 | { kind: "state"; project: string }
32 | { kind: "me"; project: string }
33 | { kind: "refresh_token"; project: string; repoName: string; ttlSeconds: number }
34 | { kind: "activity"; project: string; after?: number; limit?: number }
35 | { kind: "join"; project: string; agent: string; ttlSeconds?: number }
36 | { kind: "claim"; project: string; packet: string; repoName?: string }
37 | {
38 kind: "usage";
39 project: string;
40 packet: string;
41 inputTokens: number;
42 outputTokens: number;
43 model?: string;
44 }
45 | { kind: "review"; project: string; packet: string; repoName: string; sha: string }
46 | { kind: "review_detail"; project: string; review: string }
47 | { kind: "review_diff"; project: string; review: string; path: string }
48 | { kind: "verdict"; project: string; packet: string; ranking: string[]; rationale: string }
49 | {
50 kind: "commit";
51 project: string;
52 packet: string;
53 repoName: string;
54 files: WorkFile[];
55 message: string;
56 };
57
58/** Narrow adapter seam: no owner HTTP route is reachable through an MCP operation. */
59export interface CoreClient {
60 authenticate(token: string, signal: AbortSignal): Promise<AgentIdentity | null>;
61 execute(token: string, operation: CoreOperation, signal: AbortSignal): Promise<unknown>;
62 /** Maps to FH-005 enrollment, using owner authority held inside the adapter. */
63 enrollAgent(
64 owner: OwnerIdentity,
65 project: string,
66 agent: string,
67 signal: AbortSignal,
68 ): Promise<EnrolledAgent>;
69}
70
71/** Public messages are fixed locally; provider exception text must never reach clients. */
72export class PublicError extends Error {
73 constructor(
74 public status: number,
75 message: string,
76 ) {
77 super(message);
78 }
79}
80export const MAX_BYTES = 1024 * 1024;
81export const CALL_TIMEOUT_MS = 25_000;
82
83export async function readBounded(
84 body: ReadableStream<Uint8Array> | null,
85 max = MAX_BYTES - 1,
86 signal?: AbortSignal,
87): Promise<string> {
88 if (!body) return "";
89 signal?.throwIfAborted();
90 const reader = body.getReader();
91 const cancel = () => {
92 void reader.cancel().catch(() => {});
93 };
94 signal?.addEventListener("abort", cancel, { once: true });
95 const chunks: Uint8Array[] = [];
96 let size = 0;
97 try {
98 while (true) {
99 const { value, done } = await reader.read();
100 signal?.throwIfAborted();
101 if (done) break;
102 size += value.byteLength;
103 if (size > max) {
104 void reader.cancel().catch(() => {});
105 throw new PublicError(413, "Payload exceeds the supported size.");
106 }
107 chunks.push(value);
108 }
109 } finally {
110 signal?.removeEventListener("abort", cancel);
111 reader.releaseLock();
112 }
113 const bytes = new Uint8Array(size);
114 let offset = 0;
115 for (const chunk of chunks) {
116 bytes.set(chunk, offset);
117 offset += chunk.length;
118 }
119 return new TextDecoder().decode(bytes);
120}
121
122/** Whole-call deadline includes authorization, core I/O, and response buffering. */
123export async function boundedCall<T>(
124 operation: (signal: AbortSignal) => Promise<T>,
125 timeoutMs = CALL_TIMEOUT_MS,
126 parent?: AbortSignal,
127): Promise<T> {
128 parent?.throwIfAborted();
129 const abort = new AbortController();
130 let onParentAbort: (() => void) | undefined;
131 let timer: ReturnType<typeof setTimeout> | undefined;
132 const timeout = new Promise<never>((_, reject) => {
133 onParentAbort = () => {
134 abort.abort();
135 reject(new PublicError(504, "Call cancelled; inspect status before retrying a write."));
136 };
137 parent?.addEventListener("abort", onParentAbort, { once: true });
138 timer = setTimeout(() => {
139 abort.abort();
140 reject(new PublicError(504, "Call timed out; inspect status before retrying a write."));
141 }, timeoutMs);
142 });
143 try {
144 return await Promise.race([operation(abort.signal), timeout]);
145 } finally {
146 clearTimeout(timer);
147 if (onParentAbort) parent?.removeEventListener("abort", onParentAbort);
148 abort.abort();
149 }
150}
151
152/** Exact FH-003/FH-005 route mapping; this Request goes to a trusted binding, never public fetch. */
153export function coreRequest(token: string, operation: CoreOperation, signal: AbortSignal): Request {
154 const component = encodeURIComponent;
155 const base = `/projects/${component(operation.project)}`;
156 let path: string;
157 let data: unknown;
158 switch (operation.kind) {
159 case "state":
160 path = `${base}/state`;
161 break;
162 case "me":
163 path = `${base}/me`;
164 break;
165 case "refresh_token":
166 path = `${base}/forks/${component(operation.repoName)}/token`;
167 data = { scope: "write", ttlSeconds: operation.ttlSeconds };
168 break;
169 case "activity": {
170 const query = new URLSearchParams();
171 if (operation.after !== undefined) query.set("after", String(operation.after));
172 if (operation.limit !== undefined) query.set("limit", String(operation.limit));
173 path = `${base}/activity?${query}`;
174 break;
175 }
176 case "join":
177 path = `${base}/join`;
178 data = { agent: operation.agent, ttlSeconds: operation.ttlSeconds };
179 break;
180 case "claim":
181 path = `${base}/packets/${component(operation.packet)}/claim`;
182 data = { repoName: operation.repoName };
183 break;
184 case "review":
185 path = `${base}/reviews`;
186 data = { repoName: operation.repoName, sha: operation.sha, packet: operation.packet };
187 break;
188 case "review_detail":
189 path = `${base}/reviews/${component(operation.review)}`;
190 break;
191 case "review_diff": {
192 const query = new URLSearchParams({ path: operation.path });
193 path = `${base}/reviews/${component(operation.review)}/diff?${query}`;
194 break;
195 }
196 case "verdict":
197 path = `${base}/races/${component(operation.packet)}/verdict`;
198 data = { ranking: operation.ranking, rationale: operation.rationale };
199 break;
200 case "usage":
201 path = `${base}/usage`;
202 data = {
203 packet: operation.packet,
204 inputTokens: operation.inputTokens,
205 outputTokens: operation.outputTokens,
206 model: operation.model,
207 };
208 break;
209 case "commit":
210 path = `${base}/forks/${component(operation.repoName)}/commit`;
211 data = { packet: operation.packet, files: operation.files, message: operation.message };
212 break;
213 }
214 const body = data === undefined ? undefined : JSON.stringify(data);
215 if (body && new TextEncoder().encode(body).byteLength > MAX_BYTES)
216 throw new PublicError(413, "Commit exceeds 1 MiB.");
217 return new Request(`https://core.invalid${path}`, {
218 method: body === undefined ? "GET" : "POST",
219 signal,
220 redirect: "manual",
221 headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
222 body,
223 });
224}
225
226/** Compose the HTTP part with trusted auth/enrollment capabilities supplied by FH-012. */
227export function createCoreClient(
228 options: Pick<CoreClient, "authenticate" | "enrollAgent"> & {
229 fetch(request: Request): Promise<Response>;
230 },
231): CoreClient {
232 return {
233 authenticate: options.authenticate,
234 enrollAgent: options.enrollAgent,
235 async execute(token, operation, signal) {
236 signal.throwIfAborted();
237 const response = await options.fetch(coreRequest(token, operation, signal));
238 // workerd supports manual redirect handling, not the Fetch "error" mode.
239 // Refuse every upstream 3xx without reading or exposing its Location/body.
240 if (response.status >= 300 && response.status < 400) {
241 void response.body?.cancel().catch(() => {});
242 throw new PublicError(502, "Core redirect refused.");
243 }
244 if (!response.ok) {
245 void response.body?.cancel().catch(() => {});
246 const status = [400, 401, 403, 404, 409, 413, 429].includes(response.status)
247 ? response.status
248 : 502;
249 throw new PublicError(
250 status,
251 status === 409
252 ? "Core conflict; refresh status before retrying."
253 : `Core request refused (${status}).`,
254 );
255 }
256 try {
257 return JSON.parse(await readBounded(response.body, MAX_BYTES - 1, signal));
258 } catch (error) {
259 if (error instanceof PublicError) throw error;
260 throw new PublicError(502, "Core returned an invalid response.");
261 }
262 },
263 };
264}
265