Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-gatekeeper/src/worker/workflow-source.ts

Snapshot at 34e3175

Containing directory

4,065 bytes · typescriptView raw
1import type { ContextResponse, WorkItem, ReadReviewResponse } from "../../../forkhorse-stable/src/index.ts";
2import type { WorkflowSource, WorkflowSubmission } from "./workflow.ts";
3
4/**
5 * Server-side transport seam, relative to one authenticated project. The adapter
6 * obtains bearer credentials through the accepted Stable provider in CoreClient.
7 */
8export interface WorkflowTransport {
9  project: string;
10  identity(): Promise<{ agent: string; work: WorkItem[]; forks: { repoName: string; createdAt: string }[] }>;
11  request(path: string, init?: RequestInit): Promise<unknown>;
12}
13
14export function workflowSource(transport: WorkflowTransport): WorkflowSource {
15  return {
16    async authorize() { return transport.identity(); },
17    async context() {
18      const identity = await transport.identity();
19      const result = await transport.request("/state") as ContextResponse;
20      // Match MCP context: /state plus the authenticated /me work summary.
21      // The dashboard's normalized view loses the core resume instructions.
22      if (!result || !result.project || !Array.isArray(identity.work)) {
23        throw new Error("Invalid project context.");
24      }
25      return { ...result, myWork: identity.work };
26    },
27    async submission(operation) {
28      const identity = await transport.identity();
29      let packets: WorkflowSubmission["packets"] = [];
30      if (operation.kind === "claim") {
31        // Core has GET /packets, but no single-packet GET. Never load /state
32        // just to choose the approval kind; apply/recovery still read full evidence.
33        const result = await transport.request("/packets") as Pick<WorkflowSubmission, "packets">;
34        if (!result || !Array.isArray(result.packets)) throw new Error("Invalid packet response.");
35        const packet = result.packets.find(candidate => candidate.id === operation.packet);
36        if (packet) {
37          if (packet.mode !== undefined && packet.mode !== "standard" && packet.mode !== "race") {
38            throw new Error("Invalid packet response.");
39          }
40          packets = [{ id: packet.id, mode: packet.mode }];
41        }
42      }
43      return {
44        project: { id: transport.project }, packets,
45        forks: operation.kind === "claim" && !operation.repoName
46          ? identity.forks.map(fork => ({ repoName: fork.repoName, createdAt: fork.createdAt, agent: identity.agent }))
47          : [],
48      };
49    },
50    async readReview(review) {
51      if (!/^[a-zA-Z0-9-]{1,128}$/.test(review)) throw new Error("Invalid review ID.");
52      return transport.request(`/reviews/${encodeURIComponent(review)}`) as Promise<ReadReviewResponse>;
53    },
54    async readReceipts(review) {
55      const result = await this.readReview(review);
56      return { receipt: result.review.receipt ?? null };
57    },
58    async execute(operation) {
59      const identity = await transport.identity();
60      let path: string;
61      let method = "POST";
62      let body: Record<string, unknown> | undefined;
63      switch (operation.kind) {
64        case "join":
65          path = "/join";
66          body = { agent: identity.agent, ttlSeconds: 60 };
67          break;
68        case "claim":
69          path = `/packets/${encodeURIComponent(operation.packet)}/claim`;
70          body = operation.repoName === undefined ? {} : { repoName: operation.repoName };
71          break;
72        case "requestReview":
73          path = "/reviews";
74          body = { packet: operation.packet, repoName: operation.repoName, sha: operation.sha };
75          break;
76        case "release":
77          path = `/packets/${encodeURIComponent(operation.packet)}/claim`;
78          method = "DELETE";
79          break;
80      }
81      // In particular, join's token-bearing response never leaves this function,
82      // is never persisted, and is never included in queue metadata or errors.
83      try {
84        await transport.request(path, { method, ...(body ? { body: JSON.stringify(body) } : {}) });
85      } catch {
86        throw new Error("Workflow request failed. Inspect context before retrying.");
87      }
88    },
89  };
90}
91