Public: anyone can read this; the crew can change it.

Crew-agent routes leave api.ts for src/http/api/agents.ts #6954

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/6329-crew-agent-routes-leave-api-ts-for-src-h → main 1 commit fork

  • Merged. mason/6329-crew-agent-routes-leave-api-ts-for-src-h was merged into main as 7c31f45.

Review

Sign in to review this change.
2 files changed +114 −94
src/http/api.ts +3 −94
@@ -305,13 +305,8 @@ import {
305305 } from "../actions/runs.ts";
306306 import {
307307 addToCrew,
308 createAgent,
309308 crewAsSeen,
310 deleteAgent,
311 getAgentView,
312 listAllAgents,
313309 removeFromCrew,
314 updateAgent,
315310 } from "../actions/crew.ts";
316311 import { assignIssue, editIssue, TAKEN_UP } from "../actions/issues.ts";
317312 import { steerRepoRun } from "../actions/runs.ts";
@@ -433,6 +428,7 @@ import { frontInput, frontJson, setFront } from "../actions/front.ts";
433428 import { COPIES_PER_DAY, makeItYours } from "../actions/make_it_yours.ts";
434429 import { passportRoutes } from "./passport_edge.ts";
435430 import { egressGrantRoutes } from "./api/egress_grants.ts";
431 import { agentRoutes } from "./api/agents.ts";
436432 import { boardRoutes } from "./api/boards.ts";
437433 import { labelsRoutes } from "./api/labels.ts";
438434 import { orgRoutes } from "./api/orgs.ts";
@@ -1107,7 +1103,7 @@ function appPatch(input: Record<string, unknown>): AppSettingsInput {
11071103 };
11081104 }
11091105
1110 function agentJson(agent: Agent) {
1106 export function agentJson(agent: Agent) {
11111107 return {
11121108 username: agent.user.username,
11131109 displayName: agent.user.displayName,
@@ -1118,21 +1114,6 @@ function agentJson(agent: Agent) {
11181114 };
11191115 }
11201116
1121 /**
1122 * An agent as `viewer` may see it: its brief, model and budget are the
1123 * owner's configuration (#946), for the forge's owner and the agent itself;
1124 * anyone else signed in sees who it is.
1125 */
1126 function agentFor(agent: Agent, viewer: User | undefined) {
1127 if (viewer?.isAdmin === true || viewer?.id === agent.user.id)
1128 return agentJson(agent);
1129 return {
1130 username: agent.user.username,
1131 displayName: agent.user.displayName,
1132 createdAt: agent.createdAt,
1133 };
1134 }
1135
11361117 /** The live head of a pull — the branch tip, or the snapshot for a fork's. */
11371118 async function pullHeadOid(ctx: Ctx, repo: Repo, pull: Pull): Promise<string> {
11381119 const ref = pull.crossRepo
@@ -6122,79 +6103,7 @@ export function apiRoutes(router: Router<Handler>): void {
61226103 return timePhase(ctx.request, "render", () => json({ items, next_cursor: null }));
61236104 });
61246105
6125 router.add("GET", "/api/v1/agents", (ctx) => {
6126 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
6127 const agents = timePhase(ctx.request, "agents", () => listAllAgents(ctx.app, ctx.auth.user));
6128 if (!agents.ok) return respondAccess(agents.error, 401);
6129 return timePhase(ctx.request, "render", () =>
6130 json({
6131 items: agents.value.map((a) => agentFor(a, ctx.auth.user)),
6132 next_cursor: null,
6133 }));
6134 });
6135
6136 router.add("POST", "/api/v1/agents", async (ctx) => {
6137 const denied = requireAccount(ctx, { adminToken: true });
6138 if (denied) return denied;
6139 const input = await body(ctx.request);
6140 const made = await createAgent(ctx.app, ctx.auth.user, {
6141 username: str(input.username),
6142 ...(typeof input.displayName === "string"
6143 ? { displayName: input.displayName }
6144 : {}),
6145 ...(typeof input.model === "string" ? { model: input.model } : {}),
6146 ...(typeof input.brief === "string" ? { brief: input.brief } : {}),
6147 ...(typeof input.budgetUsd === "number"
6148 ? { budgetUsd: input.budgetUsd }
6149 : {}),
6150 });
6151 if (!made.ok) return respondAccess(made.error, 422);
6152 return json(agentJson(made.value), 201);
6153 });
6154
6155 router.add("GET", "/api/v1/agents/:username", (ctx) => {
6156 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
6157 const agent = timePhase(ctx.request, "agent", () =>
6158 getAgentView(ctx.app, ctx.auth.user, ctx.params.username ?? ""),
6159 );
6160 if (!agent.ok) return respondAccess(agent.error, 404);
6161 const view = agent.value;
6162 return timePhase(ctx.request, "render", () =>
6163 json(agentFor(view, ctx.auth.user)),
6164 );
6165 });
6166
6167 router.add("PATCH", "/api/v1/agents/:username", async (ctx) => {
6168 const denied = requireAccount(ctx, { adminToken: true });
6169 if (denied) return denied;
6170 const input = await body(ctx.request);
6171 const updated = updateAgent(
6172 ctx.app,
6173 ctx.auth.user,
6174 ctx.params.username ?? "",
6175 {
6176 ...(typeof input.model === "string" ? { model: input.model } : {}),
6177 ...(typeof input.brief === "string" ? { brief: input.brief } : {}),
6178 ...(typeof input.budgetUsd === "number"
6179 ? { budgetUsd: input.budgetUsd }
6180 : {}),
6181 },
6182 );
6183 if (!updated.ok) return respondAccess(updated.error, 422);
6184 return json(agentJson(updated.value));
6185 });
6186
6187 router.add("DELETE", "/api/v1/agents/:username", (ctx) => {
6188 const denied = requireAccount(ctx, { adminToken: true });
6189 if (denied) return denied;
6190 const removed = deleteAgent(
6191 ctx.app,
6192 ctx.auth.user,
6193 ctx.params.username ?? "",
6194 );
6195 if (!removed.ok) return respondAccess(removed.error, 404);
6196 return json({}, 200);
6197 });
6106 agentRoutes(router);
61986107
61996108 router.add("GET", "/api/v1/repos/:owner/:name/crew", (ctx) => {
62006109 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
src/http/api/agents.ts added +111
@@ -0,0 +1,111 @@
1 /**
2 * The forge's crew agents over the API: `GET` and `POST /api/v1/agents`, and
3 * `GET`, `PATCH` and `DELETE /api/v1/agents/:username`. Moved out of `api.ts`
4 * (#6329); the rules live in `actions/crew.ts`.
5 */
6
7 import {
8 createAgent,
9 deleteAgent,
10 getAgentView,
11 listAllAgents,
12 updateAgent,
13 } from "../../actions/crew.ts";
14 import type { Agent } from "../../core/agents.ts";
15 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
16 import type { User } from "../../core/users.ts";
17 import { agentJson } from "../api.ts";
18 import type { Handler } from "../context.ts";
19 import { body, json, requireAccount, respondAccess, str } from "../respond.ts";
20 import type { Router } from "../router.ts";
21
22 export function agentRoutes(router: Router<Handler>): void {
23 router.add("GET", "/api/v1/agents", (ctx) => {
24 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
25 const agents = timePhase(ctx.request, "agents", () => listAllAgents(ctx.app, ctx.auth.user));
26 if (!agents.ok) return respondAccess(agents.error, 401);
27 return timePhase(ctx.request, "render", () =>
28 json({
29 items: agents.value.map((a) => agentFor(a, ctx.auth.user)),
30 next_cursor: null,
31 }));
32 });
33
34 router.add("POST", "/api/v1/agents", async (ctx) => {
35 const denied = requireAccount(ctx, { adminToken: true });
36 if (denied) return denied;
37 const input = await body(ctx.request);
38 const made = await createAgent(ctx.app, ctx.auth.user, {
39 username: str(input.username),
40 ...(typeof input.displayName === "string"
41 ? { displayName: input.displayName }
42 : {}),
43 ...(typeof input.model === "string" ? { model: input.model } : {}),
44 ...(typeof input.brief === "string" ? { brief: input.brief } : {}),
45 ...(typeof input.budgetUsd === "number"
46 ? { budgetUsd: input.budgetUsd }
47 : {}),
48 });
49 if (!made.ok) return respondAccess(made.error, 422);
50 return json(agentJson(made.value), 201);
51 });
52
53 router.add("GET", "/api/v1/agents/:username", (ctx) => {
54 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
55 const agent = timePhase(ctx.request, "agent", () =>
56 getAgentView(ctx.app, ctx.auth.user, ctx.params.username ?? ""),
57 );
58 if (!agent.ok) return respondAccess(agent.error, 404);
59 const view = agent.value;
60 return timePhase(ctx.request, "render", () =>
61 json(agentFor(view, ctx.auth.user)),
62 );
63 });
64
65 router.add("PATCH", "/api/v1/agents/:username", async (ctx) => {
66 const denied = requireAccount(ctx, { adminToken: true });
67 if (denied) return denied;
68 const input = await body(ctx.request);
69 const updated = updateAgent(
70 ctx.app,
71 ctx.auth.user,
72 ctx.params.username ?? "",
73 {
74 ...(typeof input.model === "string" ? { model: input.model } : {}),
75 ...(typeof input.brief === "string" ? { brief: input.brief } : {}),
76 ...(typeof input.budgetUsd === "number"
77 ? { budgetUsd: input.budgetUsd }
78 : {}),
79 },
80 );
81 if (!updated.ok) return respondAccess(updated.error, 422);
82 return json(agentJson(updated.value));
83 });
84
85 router.add("DELETE", "/api/v1/agents/:username", (ctx) => {
86 const denied = requireAccount(ctx, { adminToken: true });
87 if (denied) return denied;
88 const removed = deleteAgent(
89 ctx.app,
90 ctx.auth.user,
91 ctx.params.username ?? "",
92 );
93 if (!removed.ok) return respondAccess(removed.error, 404);
94 return json({}, 200);
95 });
96 }
97
98 /**
99 * An agent as `viewer` may see it: its brief, model and budget are the
100 * owner's configuration (#946), for the forge's owner and the agent itself;
101 * anyone else signed in sees who it is.
102 */
103 function agentFor(agent: Agent, viewer: User | undefined) {
104 if (viewer?.isAdmin === true || viewer?.id === agent.user.id)
105 return agentJson(agent);
106 return {
107 username: agent.user.username,
108 displayName: agent.user.displayName,
109 createdAt: agent.createdAt,
110 };
111 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs