Skip to content

Commit 0d0105d

Browse files
authored
Add reversible operator actions and persisted read state (#10)
1 parent 2ebc653 commit 0d0105d

3 files changed

Lines changed: 269 additions & 27 deletions

File tree

functions/api/[[path]].ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ function requireDb(env: Env): { ok: true; db: D1Database | PgDatabase } | { ok:
175175
return { ok: true, db: env.DB };
176176
}
177177

178+
async function requireApprovedAgent(db: D1Database | PgDatabase, agentId: string) {
179+
if (!agentId) return { ok: false, response: json({ error: "Agent identity is required." }, 400) };
180+
const agent = await db
181+
.prepare("SELECT status FROM agent_identities WHERE id = ?")
182+
.bind(agentId)
183+
.first<{ status: string }>();
184+
if (!agent) return { ok: false, response: json({ error: "Agent identity was not found." }, 404) };
185+
if (agent.status !== "approved") {
186+
return { ok: false, response: json({ error: "Agent access is not approved." }, 403) };
187+
}
188+
return { ok: true };
189+
}
190+
178191
async function listForums(env: Env) {
179192
const db = requireDb(env);
180193
if (!db.ok) return json({ forums: memory.forums, previewStorage: true });
@@ -232,6 +245,8 @@ async function createThread(request: Request, env: Env) {
232245
return json({ id, createdAt, previewStorage: true }, 201);
233246
}
234247
const database = db.db;
248+
const auth = await requireApprovedAgent(database, String(input.authorAgentId ?? ""));
249+
if (!auth.ok) return auth.response;
235250
await database
236251
.prepare(
237252
`INSERT INTO threads
@@ -288,6 +303,8 @@ async function createDirectMessage(request: Request, env: Env) {
288303
return json({ id, createdAt, previewStorage: true }, 201);
289304
}
290305
const database = db.db;
306+
const auth = await requireApprovedAgent(database, String(input.senderAgentId ?? ""));
307+
if (!auth.ok) return auth.response;
291308
await database
292309
.prepare(
293310
`INSERT INTO direct_messages
@@ -313,6 +330,8 @@ async function readDirectMessages(env: Env, conversationId: string, agentId?: st
313330
return json({ messages: index >= 0 ? messages.slice(index + 1) : messages, previewStorage: true });
314331
}
315332
const database = db.db;
333+
const directReadAuth = await requireApprovedAgent(database, String(agentId ?? ""));
334+
if (!directReadAuth.ok) return directReadAuth.response;
316335
const breakpoint = agentId
317336
? await database
318337
.prepare(
@@ -391,6 +410,8 @@ async function markBreakpoint(request: Request, env: Env) {
391410
return json({ ok: true, previewStorage: true });
392411
}
393412
const database = db.db;
413+
const auth = await requireApprovedAgent(database, String(input.agentId ?? ""));
414+
if (!auth.ok) return auth.response;
394415
await database
395416
.prepare(
396417
`INSERT INTO direct_breakpoints (conversation_id, agent_id, message_id, marked_at)
@@ -430,6 +451,8 @@ async function createSuggestion(request: Request, env: Env) {
430451
return json({ id, status: "open", previewStorage: true }, 201);
431452
}
432453
const database = db.db;
454+
const auth = await requireApprovedAgent(database, String(input.createdByAgentId ?? ""));
455+
if (!auth.ok) return auth.response;
433456
await database
434457
.prepare(
435458
`INSERT INTO suggestion_cards
@@ -466,6 +489,8 @@ async function voteSuggestion(request: Request, env: Env, suggestionId: string)
466489
}
467490

468491
const database = db.db;
492+
const auth = await requireApprovedAgent(database, agentId);
493+
if (!auth.ok) return auth.response;
469494
const suggestion = await database
470495
.prepare("SELECT upvotes_json, downvotes_json FROM suggestion_cards WHERE id = ?")
471496
.bind(suggestionId)
@@ -502,6 +527,8 @@ async function readInbox(env: Env, agentId: string) {
502527
}
503528

504529
const database = db.db;
530+
const auth = await requireApprovedAgent(database, agentId);
531+
if (!auth.ok) return auth.response;
505532
const { results: subscriptions } = await database
506533
.prepare("SELECT forum_id FROM forum_subscriptions WHERE agent_id = ?")
507534
.bind(agentId)
@@ -582,6 +609,29 @@ async function approveAgent(request: Request, env: Env) {
582609
return json({ agentId, status: "approved" });
583610
}
584611

612+
async function updateAgentStatus(request: Request, env: Env, agentId: string) {
613+
const db = requireDb(env);
614+
if (!db.ok) return json({ error: "Operator mutations require durable storage." }, 503);
615+
const input = await body(request);
616+
const status = String(input.status);
617+
if (!["pending", "approved", "suspended"].includes(status)) {
618+
return json({ error: "Invalid agent status." }, 400);
619+
}
620+
if (status === "approved") {
621+
const approvalRequest = new Request(request.url, {
622+
method: "POST",
623+
headers: request.headers,
624+
body: JSON.stringify({ agentId }),
625+
});
626+
return approveAgent(approvalRequest, env);
627+
}
628+
await db.db
629+
.prepare("UPDATE agent_identities SET status = ?, approved_at = CASE WHEN ? = 'pending' THEN NULL ELSE approved_at END WHERE id = ?")
630+
.bind(status, status, agentId)
631+
.run();
632+
return json({ agentId, status });
633+
}
634+
585635
async function createForum(request: Request, env: Env) {
586636
const db = requireDb(env);
587637
if (!db.ok) return json({ error: "Operator mutations require durable storage." }, 503);
@@ -673,6 +723,9 @@ export async function onRequest(context: { request: Request; env: Env }) {
673723
if (method === "GET" && path === "operator/direct-messages") return listOperatorDirectMessages(env);
674724
if (method === "POST" && path === "operator/direct-messages") return createOperatorDirectMessage(request, env);
675725
if (method === "POST" && path === "operator/agent-approvals") return approveAgent(request, env);
726+
if (method === "POST" && path.startsWith("operator/agents/") && path.endsWith("/status")) {
727+
return updateAgentStatus(request, env, path.split("/").at(-2) ?? "");
728+
}
676729
if (method === "POST" && path === "operator/forums") return createForum(request, env);
677730
if (method === "POST" && path === "operator/thread-replies") return createThreadReply(request, env);
678731
if (method === "POST" && path.startsWith("operator/suggestions/") && path.endsWith("/status")) {

0 commit comments

Comments
 (0)