feat: dispatch authenticated project issue requests

This commit is contained in:
-Puter
2026-07-24 02:00:02 +05:30
parent 7974bd5f3e
commit 38c73847e0
11 changed files with 806 additions and 65 deletions

View File

@@ -14,10 +14,12 @@
"dependencies": {
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@code/primitives": "workspace:*",
"@flue/runtime": "latest",
"@rivet-dev/agentos-core": "catalog:",
"convex": "catalog:",
"hono": "4.12.30",
"effect": "catalog:",
"hono": "4.12.31",
"valibot": "^1.4.2"
},
"devDependencies": {

View File

@@ -3,6 +3,8 @@ import { registerProvider } from "@flue/runtime";
import { flue } from "@flue/runtime/routing";
import { Hono } from "hono";
import { projectRequestRoute } from "./project-request";
const agentEnv = parseAgentEnv(process.env);
registerProvider(agentEnv.AGENT_MODEL_PROVIDER, {
@@ -18,6 +20,7 @@ registerProvider(agentEnv.AGENT_MODEL_PROVIDER, {
});
const app = new Hono();
app.post("/project-requests", projectRequestRoute);
app.route("/", flue());
export default app;

View File

@@ -98,7 +98,9 @@ const { CONVEX_URL } = parseAgentEnv(process.env);
* caller's JWT is set on this instance only and discarded when the request
* ends. We never mutate auth on a shared/global Convex client.
*/
const createAuthenticatedClient = (accessToken: string): ConvexHttpClient => {
export const createAuthenticatedClient = (
accessToken: string
): ConvexHttpClient => {
const client = new ConvexHttpClient(CONVEX_URL);
client.setAuth(accessToken);
return client;
@@ -120,7 +122,7 @@ interface HeaderSource {
* Extract and validate the Bearer access token from the request. Returns null
* when absent or malformed so the caller can produce a clean 401.
*/
const extractBearerToken = (request: HeaderSource): string | null => {
export const extractBearerToken = (request: HeaderSource): string | null => {
const header = request.headers.get("authorization");
if (!header) {
return null;

View File

@@ -0,0 +1,162 @@
import {
decodeProjectIssueRequest,
ProjectIssueDispatchInput,
ProjectIssueRequestError,
ProjectIssueRequestResult,
ProjectIssueValidationError,
} from "@code/primitives/project-issue";
import type { ProjectIssueRequest } from "@code/primitives/project-issue";
import { dispatch } from "@flue/runtime";
import type { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import { Effect, Schema } from "effect";
import type { Context } from "hono";
import { createAuthenticatedClient, extractBearerToken } from "./auth";
interface ProjectIssueCreateArgs extends Record<string, unknown> {
readonly body: string;
readonly projectId: string;
readonly title: string;
}
const createIssue = makeFunctionReference<
"mutation",
ProjectIssueCreateArgs,
string
>("projectIssues:create");
const createIssueFromSignal = makeFunctionReference<
"mutation",
{ readonly signalId: string },
{ readonly issueId: string; readonly projectId: string }
>("projectIssues:createFromSignal");
const beginIssue = makeFunctionReference<
"mutation",
{ readonly issueId: string },
"queued" | "working"
>("projectIssues:begin");
const markDispatchFailed = makeFunctionReference<
"mutation",
{ readonly error: string; readonly issueId: string },
null
>("projectIssues:markDispatchFailed");
const invalidRequest = (message: string) =>
Response.json({ error: message }, { status: 400 });
const knownAuthorizationFailure = (message: string): boolean =>
/authentication required|membership required|project not found|signal not found|not project-scoped/iu.test(
message
);
const errorMessage = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
const decodeRequest = async (input: unknown): Promise<ProjectIssueRequest> => {
try {
return await Effect.runPromise(decodeProjectIssueRequest(input));
} catch (error) {
if (
error instanceof ProjectIssueRequestError ||
error instanceof ProjectIssueValidationError
) {
throw invalidRequest(
error instanceof Error ? error.message : "Invalid project request"
);
}
throw error;
}
};
const createIssueForRequest = async (
client: ConvexHttpClient,
request: Awaited<ReturnType<typeof decodeRequest>>
): Promise<{ readonly issueId: string; readonly projectId: string }> => {
if (request.kind === "signal") {
return client.mutation(createIssueFromSignal, {
signalId: request.signalId,
});
}
const issueId = await client.mutation(createIssue, {
body: request.body,
projectId: request.projectId,
title: request.title,
});
return { issueId, projectId: request.projectId };
};
export const projectRequestRoute = async (c: Context): Promise<Response> => {
const accessToken = extractBearerToken(c.req.raw);
if (!accessToken) {
return c.json({ error: "Unauthorized" }, 401);
}
let input: unknown;
try {
input = await c.req.json();
} catch {
return invalidRequest("Request body must be valid JSON");
}
let request: Awaited<ReturnType<typeof decodeRequest>>;
try {
request = await decodeRequest(input);
} catch (error) {
if (error instanceof Response) {
return error;
}
return c.json({ error: "Invalid project request" }, 400);
}
const client = createAuthenticatedClient(accessToken);
let issue:
| { readonly issueId: string; readonly projectId: string }
| undefined;
try {
issue = await createIssueForRequest(client, request);
const status = await client.mutation(beginIssue, {
issueId: issue.issueId,
});
const dispatchInput = Schema.decodeUnknownSync(ProjectIssueDispatchInput)({
issueId: issue.issueId,
kind: "project.issue.started",
projectId: issue.projectId,
});
const receipt = await dispatch({
agent: "project-manager",
id: issue.issueId,
input: dispatchInput,
});
const result = Schema.decodeUnknownSync(ProjectIssueRequestResult)({
acceptedAt: receipt.acceptedAt,
dispatchId: receipt.dispatchId,
issueId: issue.issueId,
projectId: issue.projectId,
status,
});
return c.json(result, 202);
} catch (error) {
const message = errorMessage(error);
if (issue) {
try {
await client.mutation(markDispatchFailed, {
error: message,
issueId: issue.issueId,
});
} catch {
// Preserve the original request failure; the issue remains inspectable.
}
}
return c.json(
{
error: knownAuthorizationFailure(message)
? "Project request is not authorized"
: "Project request could not be dispatched",
},
knownAuthorizationFailure(message) ? 403 : 502
);
}
};