-
Notifications
You must be signed in to change notification settings - Fork 1.4k
refactor: parse every remaining I/O boundary into a domain type (CMP-82) #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b44c479
318efc2
784c5a6
288b1b7
5a52210
cf90568
e360f15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ import { timingSafeEqual } from "node:crypto"; | |
| import { EnrichmentStatus, Prisma } from "@crm/db"; | ||
| import { MAX_ATTEMPTS } from "@crm/db/agent-tasks"; | ||
| import { schemas } from "@crm/validation"; | ||
| import { eveTurnFailure } from "@crm/validation/eve-stream"; | ||
| import { defineChannel, GET, POST } from "eve/channels"; | ||
| import { z } from "zod"; | ||
| import { persistBuilderInputRequest } from "../lib/builder-input"; | ||
| import { verifyKey } from "../lib/context-dev"; | ||
| import { | ||
|
|
@@ -26,14 +28,36 @@ import { | |
| } from "../lib/dispatch"; | ||
| import { DISPATCH } from "../lib/dispatch-config"; | ||
| import { settle } from "../lib/enrichment"; | ||
| import { finishRun } from "../lib/run-runtime"; | ||
| import { finishRun, runResultOf } from "../lib/run-runtime"; | ||
| import { attribute } from "../lib/session-purpose"; | ||
| import { createSlackChannel } from "../lib/slack-membership"; | ||
| import { completeTask, taskSubject } from "../lib/tasks"; | ||
|
|
||
| const TASK_MARKER = "task:"; | ||
| const STALE_QUEUE_MS = DISPATCH.sweep.staleQueueMs; | ||
|
|
||
| type InternalDispatchPrincipal = { | ||
| readonly authenticator: string; | ||
| readonly principalId: string; | ||
| readonly principalType: string; | ||
| } | null; | ||
|
|
||
| const identifier = z.string().trim().min(1).nullable().catch(null); | ||
|
|
||
| const cancelRunRequest = z.object({ runId: identifier }).catch({ runId: null }); | ||
|
|
||
| const verifyKeyRequest = z | ||
| .object({ apiKey: identifier }) | ||
| .catch({ apiKey: null }); | ||
|
|
||
| const receiveTarget = z | ||
| .object({ | ||
| builderSubmissionId: z.string().nullable().catch(null), | ||
| runId: z.string().nullable().catch(null), | ||
| taskId: z.string().nullable().catch(null), | ||
| }) | ||
| .catch({ builderSubmissionId: null, runId: null, taskId: null }); | ||
|
|
||
| function authorised(request: Request): boolean { | ||
| const secret = process.env.AGENT_BRIDGE_SECRET?.trim(); | ||
| if (!secret) return false; | ||
|
|
@@ -140,10 +164,9 @@ export default defineChannel({ | |
| return new Response("Unauthorized", { status: 401 }); | ||
| } | ||
|
|
||
| const body = (await request.json().catch(() => null)) as { | ||
| runId?: unknown; | ||
| } | null; | ||
| const runId = typeof body?.runId === "string" ? body.runId.trim() : null; | ||
| const { runId } = cancelRunRequest.parse( | ||
| await request.json().catch(() => null), | ||
| ); | ||
| if (!runId) { | ||
| return Response.json({ error: "No run id was sent." }, { status: 400 }); | ||
| } | ||
|
|
@@ -184,12 +207,9 @@ export default defineChannel({ | |
| return new Response("Unauthorized", { status: 401 }); | ||
| } | ||
|
|
||
| const body = (await request.json().catch(() => null)) as { | ||
| apiKey?: unknown; | ||
| } | null; | ||
|
|
||
| const apiKey = | ||
| typeof body?.apiKey === "string" ? body.apiKey.trim() : null; | ||
| const { apiKey } = verifyKeyRequest.parse( | ||
| await request.json().catch(() => null), | ||
| ); | ||
|
|
||
| if (!apiKey) { | ||
| return Response.json( | ||
|
|
@@ -249,9 +269,7 @@ export default defineChannel({ | |
| async "turn.failed"(data, channel) { | ||
| const taskId = taskFromToken(channel.continuationToken); | ||
| const reason = | ||
| typeof data === "object" && data && "message" in data | ||
| ? String((data as { message: unknown }).message) | ||
| : "The agent turn failed."; | ||
| eveTurnFailure.parse(data).message ?? "The agent turn failed."; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When Prompt for AI agents |
||
|
|
||
| if (taskId) { | ||
| const subject = await taskSubject(taskId); | ||
|
|
@@ -300,7 +318,7 @@ export default defineChannel({ | |
| try { | ||
| await finishRun(runId, { | ||
| summary: run.summary ?? "The agent run completed.", | ||
| result: recordOf(run.result), | ||
| result: runResultOf(run.result), | ||
| }); | ||
| } catch (error) { | ||
| await failRun( | ||
|
|
@@ -357,47 +375,32 @@ export default defineChannel({ | |
| }, | ||
|
|
||
| async receive(input, { send }) { | ||
| const builderSubmissionId = | ||
| typeof input.target?.builderSubmissionId === "string" | ||
| ? input.target.builderSubmissionId | ||
| : null; | ||
| if (builderSubmissionId) { | ||
| const target = receiveTarget.parse(input.target); | ||
| if (target.builderSubmissionId) { | ||
| assertInternalDispatchAuth(input.auth); | ||
| return dispatchBuilderSubmission(builderSubmissionId, send); | ||
| return dispatchBuilderSubmission(target.builderSubmissionId, send); | ||
| } | ||
|
|
||
| const runId = | ||
| typeof input.target?.runId === "string" ? input.target.runId : null; | ||
| if (runId) { | ||
| if (target.runId) { | ||
| assertInternalDispatchAuth(input.auth); | ||
| return dispatchAgentRun(runId, send); | ||
| return dispatchAgentRun(target.runId, send); | ||
| } | ||
|
|
||
| const taskId = | ||
| typeof input.target?.taskId === "string" ? input.target.taskId : null; | ||
|
|
||
| return send(input.message, { | ||
| auth: input.auth, | ||
| continuationToken: taskId | ||
| ? taskToken(taskId) | ||
| continuationToken: target.taskId | ||
| ? taskToken(target.taskId) | ||
| : `crm:adhoc:${crypto.randomUUID()}`, | ||
| }); | ||
| }, | ||
| }); | ||
|
|
||
| function assertInternalDispatchAuth(value: unknown): void { | ||
| const auth = recordOf(value); | ||
| function assertInternalDispatchAuth(auth: InternalDispatchPrincipal): void { | ||
| if ( | ||
| auth.authenticator !== "app" || | ||
| auth?.authenticator !== "app" || | ||
| auth.principalType !== "runtime" || | ||
| auth.principalId !== "eve:app" | ||
| ) { | ||
| throw new Error("Internal agent dispatch requires Eve app authentication."); | ||
| } | ||
| } | ||
|
|
||
| function recordOf(value: unknown): Record<string, unknown> { | ||
| return value && typeof value === "object" && !Array.isArray(value) | ||
| ? (value as Record<string, unknown>) | ||
| : {}; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,11 @@ | ||||||||
| import { defineHook, type HookEvent } from "eve/hooks"; | ||||||||
| import { z } from "zod"; | ||||||||
|
|
||||||||
| type ActionRequest = HookEvent<"actions.requested">["data"]["actions"][number]; | ||||||||
| type ActionResult = HookEvent<"action.result">["data"]["result"]; | ||||||||
| type ActionInput = ActionRequest["input"]; | ||||||||
|
|
||||||||
| const inputText = z.string().nullable().catch(null); | ||||||||
|
|
||||||||
| const SHOW_CONTENT = process.env.NODE_ENV !== "production"; | ||||||||
| const MAX_IN_FLIGHT = 256; | ||||||||
|
|
@@ -16,14 +20,14 @@ function line(symbol: string, text: string): void { | |||||||
| console.error(`[agent] ${symbol} ${text}`); | ||||||||
| } | ||||||||
|
|
||||||||
| function preview(input: unknown): string { | ||||||||
| if (!SHOW_CONTENT || typeof input !== "object" || input === null) return ""; | ||||||||
| function preview(input: ActionInput): string { | ||||||||
| if (!SHOW_CONTENT) return ""; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The refactor removed the Prompt for AI agents
Suggested change
|
||||||||
|
|
||||||||
| const parts: string[] = []; | ||||||||
|
|
||||||||
| for (const [key, value] of Object.entries(input)) { | ||||||||
| if (value === null || value === undefined) continue; | ||||||||
| const text = typeof value === "string" ? value : JSON.stringify(value); | ||||||||
| const text = inputText.parse(value) ?? JSON.stringify(value); | ||||||||
| parts.push(`${key}=${truncate(text ?? String(value), 48)}`); | ||||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,13 @@ import { db } from "@crm/db"; | |
| import { readAgentModel } from "@crm/db/settings"; | ||
| import { agentError, modelError } from "@crm/telemetry"; | ||
| import { defineHook } from "eve/hooks"; | ||
| import { z } from "zod"; | ||
|
|
||
| type SessionPrincipal = { | ||
| readonly attributes?: Readonly<Record<string, string | readonly string[]>>; | ||
| } | null; | ||
|
|
||
| const attributeText = z.string().trim().min(1).nullable().catch(null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This change re-declares two things already defined in apps/agent/agent/lib/session-purpose.ts: the session attributes type ( Prompt for AI agents |
||
|
|
||
| let modelId: string | null = null; | ||
|
|
||
|
|
@@ -27,11 +34,8 @@ const MODEL_CODES = [ | |
| "unauthorized", | ||
| ]; | ||
|
|
||
| function taskKind( | ||
| auth: { attributes?: Record<string, unknown> } | null, | ||
| ): string | null { | ||
| const kind = auth?.attributes?.taskKind; | ||
| return typeof kind === "string" && kind.trim() ? kind.trim() : null; | ||
| function taskKind(auth: SessionPrincipal): string | null { | ||
| return attributeText.parse(auth?.attributes?.taskKind); | ||
| } | ||
|
|
||
| function looksLikeModel(code: string): boolean { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This disables
no-unknown-parametersfor every future module added under packages/validation/src, not just the files that legitimately decodeunknownat their entry points. A new non-boundary function that acceptsunknownanywhere in the package will silently pass lint, contradicting the boundary-decoder contract the package exists to enforce. Narrow the override to the specific files that parseunknown(agent-manifest.ts, and index.ts'sparse) instead.Prompt for AI agents