theokit 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actions-virtual-module-3CDQTWOC.js → actions-virtual-module-G4BANOLW.js} +5 -3
- package/dist/{actions-virtual-module-3CDQTWOC.js.map → actions-virtual-module-G4BANOLW.js.map} +1 -1
- package/dist/agent-MN7XGJR3.js +209 -0
- package/dist/agent-MN7XGJR3.js.map +1 -0
- package/dist/{app-typed-client-CSOK7NPC.js → app-typed-client-Z6BHD4MF.js} +5 -3
- package/dist/{app-typed-client-CSOK7NPC.js.map → app-typed-client-Z6BHD4MF.js.map} +1 -1
- package/dist/{build-5K7LK77K.js → build-QDAFSKKW.js} +11 -7
- package/dist/{build-5K7LK77K.js.map → build-QDAFSKKW.js.map} +1 -1
- package/dist/chunk-34YQOXGM.js +37 -0
- package/dist/chunk-34YQOXGM.js.map +1 -0
- package/dist/{chunk-7YZHAQU7.js → chunk-567NA7Y6.js} +8 -99
- package/dist/chunk-567NA7Y6.js.map +1 -0
- package/dist/{chunk-BQDGES7C.js → chunk-GBXLKYIA.js} +19 -43
- package/dist/chunk-GBXLKYIA.js.map +1 -0
- package/dist/chunk-GDN3PXFH.js +101 -0
- package/dist/chunk-GDN3PXFH.js.map +1 -0
- package/dist/chunk-NBWB4S46.js +81 -0
- package/dist/chunk-NBWB4S46.js.map +1 -0
- package/dist/{chunk-F4YUPDJ2.js → chunk-NXTF5PPW.js} +15 -41
- package/dist/chunk-NXTF5PPW.js.map +1 -0
- package/dist/{chunk-S7Y5WLZY.js → chunk-OTFIRP6S.js} +76 -46
- package/dist/chunk-OTFIRP6S.js.map +1 -0
- package/dist/chunk-P37RZRFV.js +31 -0
- package/dist/chunk-P37RZRFV.js.map +1 -0
- package/dist/{chunk-RBHCJHRR.js → chunk-UOR6JTCI.js} +142 -7
- package/dist/chunk-UOR6JTCI.js.map +1 -0
- package/dist/{chunk-637WJB7Z.js → chunk-ZJDKAD3L.js} +55 -14
- package/dist/chunk-ZJDKAD3L.js.map +1 -0
- package/dist/cli/index.js +22 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/{dev-QOEVYNBG.js → dev-TEE4T6ZB.js} +13 -8
- package/dist/{dev-QOEVYNBG.js.map → dev-TEE4T6ZB.js.map} +1 -1
- package/dist/{dev-emit-5MDSBP5D.js → dev-emit-VJ5CFMPY.js} +5 -3
- package/dist/{dev-emit-5MDSBP5D.js.map → dev-emit-VJ5CFMPY.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/{info-OUEUZOT7.js → info-7PE2PZJI.js} +6 -5
- package/dist/{info-OUEUZOT7.js.map → info-7PE2PZJI.js.map} +1 -1
- package/dist/{internal-api-EFKZWIYZ.js → internal-api-J27TYE2I.js} +7 -4
- package/dist/load-config-JKYO5RFK.js +14 -0
- package/dist/{openapi-FHY6HC6I.js → openapi-MXMLZCXC.js} +7 -4
- package/dist/{openapi-FHY6HC6I.js.map → openapi-MXMLZCXC.js.map} +1 -1
- package/dist/{registry-34LL7NF4.js → registry-XJUYD2OU.js} +2 -2
- package/dist/{routes-EW7TP7NJ.js → routes-NNBEZSGN.js} +5 -3
- package/dist/{routes-EW7TP7NJ.js.map → routes-NNBEZSGN.js.map} +1 -1
- package/dist/{start-2KG4JSXM.js → start-7MQEEQH4.js} +68 -8
- package/dist/start-7MQEEQH4.js.map +1 -0
- package/dist/{static-55G3LX2I.js → static-7ARBVDJF.js} +4 -4
- package/dist/vite-plugin/index.js +1 -1
- package/dist/{vite-plugin-TDIDZ5U7.js → vite-plugin-GC6WCU4P.js} +11 -7
- package/dist/vite-plugin-GC6WCU4P.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-637WJB7Z.js.map +0 -1
- package/dist/chunk-7YZHAQU7.js.map +0 -1
- package/dist/chunk-BQDGES7C.js.map +0 -1
- package/dist/chunk-F4YUPDJ2.js.map +0 -1
- package/dist/chunk-RBHCJHRR.js.map +0 -1
- package/dist/chunk-S7Y5WLZY.js.map +0 -1
- package/dist/start-2KG4JSXM.js.map +0 -1
- /package/dist/{internal-api-EFKZWIYZ.js.map → internal-api-J27TYE2I.js.map} +0 -0
- /package/dist/{vite-plugin-TDIDZ5U7.js.map → load-config-JKYO5RFK.js.map} +0 -0
- /package/dist/{registry-34LL7NF4.js.map → registry-XJUYD2OU.js.map} +0 -0
- /package/dist/{static-55G3LX2I.js.map → static-7ARBVDJF.js.map} +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "tsx/esm";
|
|
3
|
+
import {
|
|
4
|
+
getApprovalRegistry
|
|
5
|
+
} from "./chunk-NBWB4S46.js";
|
|
3
6
|
import {
|
|
4
7
|
validateCsrfRequest
|
|
5
8
|
} from "./chunk-WR4F4EEZ.js";
|
|
@@ -41,8 +44,63 @@ function resolveTransformer(selector) {
|
|
|
41
44
|
return selector;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
// src/server/agent/approve-agent.ts
|
|
48
|
+
var APPROVE_SEGMENT = "/approve/";
|
|
49
|
+
function parseApprovalId(urlPath) {
|
|
50
|
+
const at = urlPath.indexOf(APPROVE_SEGMENT);
|
|
51
|
+
if (at === -1) return null;
|
|
52
|
+
const id = urlPath.slice(at + APPROVE_SEGMENT.length);
|
|
53
|
+
return id.length > 0 && !id.includes("/") ? id : null;
|
|
54
|
+
}
|
|
55
|
+
function isApprovalPath(urlPath) {
|
|
56
|
+
return urlPath.includes(APPROVE_SEGMENT);
|
|
57
|
+
}
|
|
58
|
+
function jsonError(status, code, message) {
|
|
59
|
+
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
60
|
+
status,
|
|
61
|
+
headers: { "content-type": "application/json" }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function parseApprovalBody(body) {
|
|
65
|
+
if (typeof body !== "object" || body === null) return null;
|
|
66
|
+
const b = body;
|
|
67
|
+
return typeof b.approved === "boolean" ? { approved: b.approved } : null;
|
|
68
|
+
}
|
|
69
|
+
async function handleAgentApproval(request, urlPath, registry, csrfMode = "strict") {
|
|
70
|
+
if (csrfMode !== "off") {
|
|
71
|
+
const csrf = validateCsrfRequest(request);
|
|
72
|
+
if (!csrf.valid && csrfMode === "strict") {
|
|
73
|
+
return jsonError(403, "CSRF_FAILED", `CSRF check failed: ${csrf.reason}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const approvalId = parseApprovalId(urlPath);
|
|
77
|
+
if (approvalId === null) {
|
|
78
|
+
return jsonError(400, "BAD_REQUEST", "Approval path must be /api/agents/<name>/approve/<id>.");
|
|
79
|
+
}
|
|
80
|
+
let body = null;
|
|
81
|
+
try {
|
|
82
|
+
body = await request.json();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
const parsed = parseApprovalBody(body);
|
|
86
|
+
if (parsed === null) {
|
|
87
|
+
return jsonError(400, "BAD_REQUEST", "Request body must contain a boolean `approved`.");
|
|
88
|
+
}
|
|
89
|
+
const resolved = registry.resolve(approvalId, parsed.approved);
|
|
90
|
+
if (!resolved) {
|
|
91
|
+
return jsonError(404, "NOT_PENDING", `No pending approval for id '${approvalId}'.`);
|
|
92
|
+
}
|
|
93
|
+
return new Response(JSON.stringify({ resolved: true }), {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { "content-type": "application/json" }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
44
99
|
// src/server/agent/mount-agent.ts
|
|
45
|
-
import {
|
|
100
|
+
import {
|
|
101
|
+
compileAgentModule,
|
|
102
|
+
streamAgentUIMessages
|
|
103
|
+
} from "@theokit/agents";
|
|
46
104
|
|
|
47
105
|
// src/server/define/ui-message-stream-response.ts
|
|
48
106
|
var UI_MESSAGE_STREAM_HEADERS = {
|
|
@@ -78,7 +136,7 @@ function partText(part) {
|
|
|
78
136
|
const p = part;
|
|
79
137
|
return p.type === "text" && typeof p.text === "string" ? p.text : "";
|
|
80
138
|
}
|
|
81
|
-
function
|
|
139
|
+
function jsonError2(status, code, message) {
|
|
82
140
|
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
83
141
|
status,
|
|
84
142
|
headers: { "content-type": "application/json" }
|
|
@@ -105,7 +163,7 @@ async function mountAgent(mod, request, apiKey, source = "agent module", csrfMod
|
|
|
105
163
|
if (csrfMode !== "off") {
|
|
106
164
|
const csrf = validateCsrfRequest(request);
|
|
107
165
|
if (!csrf.valid && csrfMode === "strict") {
|
|
108
|
-
return
|
|
166
|
+
return jsonError2(403, "CSRF_FAILED", `CSRF check failed: ${csrf.reason}`);
|
|
109
167
|
}
|
|
110
168
|
}
|
|
111
169
|
const compiled = compileAgentModule(mod, source);
|
|
@@ -116,48 +174,19 @@ async function mountAgent(mod, request, apiKey, source = "agent module", csrfMod
|
|
|
116
174
|
}
|
|
117
175
|
const input = parseAgentRequestBody(body);
|
|
118
176
|
if (input === null) {
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
return uiMessageStreamResponse(streamAgentUIMessages(compiled, apiKey, input));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/server/agent/provider-resolver.ts
|
|
125
|
-
var DEFAULT_REGISTRY = [
|
|
126
|
-
{
|
|
127
|
-
name: "openrouter",
|
|
128
|
-
envKey: "OPENROUTER_API_KEY",
|
|
129
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
130
|
-
priority: 1
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "openai",
|
|
134
|
-
envKey: "OPENAI_API_KEY",
|
|
135
|
-
baseUrl: "https://api.openai.com/v1",
|
|
136
|
-
priority: 2
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: "anthropic",
|
|
140
|
-
envKey: "ANTHROPIC_API_KEY",
|
|
141
|
-
baseUrl: "https://api.anthropic.com",
|
|
142
|
-
priority: 3
|
|
143
|
-
}
|
|
144
|
-
];
|
|
145
|
-
var registry = [...DEFAULT_REGISTRY];
|
|
146
|
-
function resolveProvider() {
|
|
147
|
-
const sorted = [...registry].sort((a, b) => a.priority - b.priority);
|
|
148
|
-
for (const desc of sorted) {
|
|
149
|
-
const apiKey = process.env[desc.envKey];
|
|
150
|
-
if (apiKey && apiKey.length > 0) {
|
|
151
|
-
return {
|
|
152
|
-
name: desc.name,
|
|
153
|
-
apiKey,
|
|
154
|
-
baseUrl: desc.baseUrl
|
|
155
|
-
};
|
|
156
|
-
}
|
|
177
|
+
return jsonError2(400, "BAD_REQUEST", "Request must contain a non-empty message or messages[].");
|
|
157
178
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
const gated = compiled.hitl;
|
|
180
|
+
const registry = getApprovalRegistry();
|
|
181
|
+
const hitl = gated && gated.size > 0 ? {
|
|
182
|
+
gated,
|
|
183
|
+
awaitApproval: (approvalId, opts) => registry.register(approvalId, {
|
|
184
|
+
timeoutMs: opts.timeout ?? 3e5,
|
|
185
|
+
onTimeout: opts.onTimeout ?? "abort"
|
|
186
|
+
})
|
|
187
|
+
} : void 0;
|
|
188
|
+
return uiMessageStreamResponse(
|
|
189
|
+
streamAgentUIMessages(compiled, apiKey, { ...input, hitl, signal: request.signal })
|
|
161
190
|
);
|
|
162
191
|
}
|
|
163
192
|
|
|
@@ -227,9 +256,10 @@ function pickHeaderString(value) {
|
|
|
227
256
|
|
|
228
257
|
export {
|
|
229
258
|
resolveTransformer,
|
|
259
|
+
isApprovalPath,
|
|
260
|
+
handleAgentApproval,
|
|
230
261
|
mountAgent,
|
|
231
|
-
resolveProvider,
|
|
232
262
|
incomingMessageToWebRequest,
|
|
233
263
|
writeWebResponseToServerResponse
|
|
234
264
|
};
|
|
235
|
-
//# sourceMappingURL=chunk-
|
|
265
|
+
//# sourceMappingURL=chunk-OTFIRP6S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/transformer.ts","../src/server/agent/approve-agent.ts","../src/server/agent/mount-agent.ts","../src/server/define/ui-message-stream-response.ts","../src/server/http/node-web-adapter.ts"],"sourcesContent":["import superjson from 'superjson'\n\n/**\n * T5.2 — pluggable response/request transformer.\n *\n * `superjson` is the default, preserving Date/Map/Set/BigInt/etc.\n * `json` is the lightweight option (plain JSON.stringify/parse).\n * Users can supply a custom object implementing this contract.\n */\nexport interface TheoTransformer {\n name: string\n serialize: (value: unknown) => string\n deserialize: (raw: string) => unknown\n}\n\nexport const superjsonTransformer: TheoTransformer = {\n name: 'superjson',\n serialize: (v) => JSON.stringify(superjson.serialize(v)),\n deserialize: (raw) => {\n const parsed = JSON.parse(raw) as Parameters<typeof superjson.deserialize>[0]\n return superjson.deserialize(parsed)\n },\n}\n\nexport const jsonTransformer: TheoTransformer = {\n name: 'json',\n serialize: (v) => JSON.stringify(v),\n deserialize: (raw) => JSON.parse(raw) as unknown,\n}\n\nconst BUILT_INS: Record<string, TheoTransformer> = {\n superjson: superjsonTransformer,\n json: jsonTransformer,\n}\n\nexport function resolveTransformer(\n selector: 'json' | 'superjson' | TheoTransformer,\n): TheoTransformer {\n if (typeof selector === 'string') {\n // selector is 'json' | 'superjson' literal — both keys exist in\n // BUILT_INS by construction. Type system guarantees a hit; we keep\n // a defensive fallback that the compiler cannot see is unreachable\n // at runtime, just in case someone adds a new literal to the union\n // but forgets to register the built-in.\n const built = BUILT_INS[selector]\n // Defensive: the public union ensures `built` is defined, but if a\n // future contributor extends the union without registering the impl,\n // the cast keeps the failure mode loud.\n if ((built as TheoTransformer | undefined) === undefined) {\n throw new Error(\n `Unknown transformer \"${selector}\". Built-in options: ${Object.keys(BUILT_INS).join(', ')}.`,\n )\n }\n return built\n }\n if (\n typeof selector !== 'object' ||\n typeof selector.serialize !== 'function' ||\n typeof selector.deserialize !== 'function'\n ) {\n throw new Error(\n `Custom transformer must have serialize and deserialize functions. Got: ${JSON.stringify(selector)}`,\n )\n }\n return selector\n}\n","/**\n * M4 (theokit-ai-first) — the HITL approve endpoint: `POST /api/agents/<name>/approve/<approvalId>`.\n *\n * The counterpart to `mountAgent`'s HITL pause. While a gated tool holds the SDK run paused (the\n * awaited `pre_tool_call` hook), the client POSTs here with `{ approved }`; this resolves the\n * pending approval in the shared registry, which un-pauses the run (allow) or vetoes the tool (deny).\n *\n * Web-Standard `Request` → `Response`, one wiring point shared by dev (vite middleware) and prod\n * (built server) so the two never drift (EC-4 parity with `mountAgent`). The registry is INJECTED —\n * dev/prod pass the process singleton (`getApprovalRegistry`), tests pass a fresh instance.\n */\nimport { validateCsrfRequest, type CsrfMode } from '../security/csrf.js'\n\nimport type { ApprovalRegistry } from './approval-registry.js'\n\n/** The path segment separating the agent name from the approval id. */\nconst APPROVE_SEGMENT = '/approve/'\n\n/**\n * Extract the `<approvalId>` from a `/api/agents/<name>/approve/<approvalId>` path.\n * Returns `null` when the path has no `/approve/` segment or an empty / nested id.\n */\nexport function parseApprovalId(urlPath: string): string | null {\n const at = urlPath.indexOf(APPROVE_SEGMENT)\n if (at === -1) return null\n const id = urlPath.slice(at + APPROVE_SEGMENT.length)\n return id.length > 0 && !id.includes('/') ? id : null\n}\n\n/** True when `urlPath` targets a HITL approve endpoint (used by dev/prod routing to branch early). */\nexport function isApprovalPath(urlPath: string): boolean {\n return urlPath.includes(APPROVE_SEGMENT)\n}\n\nfunction jsonError(status: number, code: string, message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/** Extract `{ approved: boolean }` from an untrusted body; `null` when the shape is wrong. */\nfunction parseApprovalBody(body: unknown): { approved: boolean } | null {\n if (typeof body !== 'object' || body === null) return null\n const b = body as Record<string, unknown>\n return typeof b.approved === 'boolean' ? { approved: b.approved } : null\n}\n\n/**\n * Resolve a pending HITL approval. CSRF-guarded like `mountAgent` (the custom `X-Theo-Action`\n * header + Origin match) — a cross-origin POST must not approve a paused tool. Returns:\n * 403 CSRF_FAILED — strict CSRF check failed\n * 400 BAD_REQUEST — no `/approve/<id>` in the path, or body lacks a boolean `approved`\n * 404 NOT_PENDING — the id is unknown or already settled (idempotent double-submit)\n * 200 { resolved:true } — the approval was settled by this call\n */\nexport async function handleAgentApproval(\n request: Request,\n urlPath: string,\n registry: ApprovalRegistry,\n csrfMode: CsrfMode = 'strict',\n): Promise<Response> {\n if (csrfMode !== 'off') {\n const csrf = validateCsrfRequest(request)\n if (!csrf.valid && csrfMode === 'strict') {\n return jsonError(403, 'CSRF_FAILED', `CSRF check failed: ${csrf.reason}`)\n }\n }\n\n const approvalId = parseApprovalId(urlPath)\n if (approvalId === null) {\n return jsonError(400, 'BAD_REQUEST', 'Approval path must be /api/agents/<name>/approve/<id>.')\n }\n\n let body: unknown = null\n try {\n body = await request.json()\n } catch {\n /* invalid/empty JSON → handled below as a 400 */\n }\n const parsed = parseApprovalBody(body)\n if (parsed === null) {\n return jsonError(400, 'BAD_REQUEST', 'Request body must contain a boolean `approved`.')\n }\n\n const resolved = registry.resolve(approvalId, parsed.approved)\n if (!resolved) {\n return jsonError(404, 'NOT_PENDING', `No pending approval for id '${approvalId}'.`)\n }\n return new Response(JSON.stringify({ resolved: true }), {\n status: 200,\n headers: { 'content-type': 'application/json' },\n })\n}\n","/**\n * M2 (theokit-ai-first) — mount a scanned `agents/<name>.ts` module as an SSE endpoint.\n *\n * The SINGLE wiring point shared by dev (vite middleware) and prod (built server), so the\n * two never drift (EC-4). Web-Standard `Request` → `Response`: parse the chat body, compile\n * the module (`compileAgentModule`, converges both surfaces), stream via the M0/M1 canonical\n * protocol (`streamAgentUIMessages` → `uiMessageStreamResponse`).\n *\n * Request body — accepts the `@ai-sdk/react` `useChat` shape (`{ id, messages: UIMessage[] }`,\n * the typed-client path) AND a simple `{ message, sessionId? }` shape (M0/M1-style clients).\n */\nimport {\n compileAgentModule,\n streamAgentUIMessages,\n type HumanInTheLoopOptions,\n} from '@theokit/agents'\n\nimport { uiMessageStreamResponse } from '../define/ui-message-stream-response.js'\nimport { validateCsrfRequest, type CsrfMode } from '../security/csrf.js'\n\nimport { getApprovalRegistry } from './approval-registry.js'\n\n/** The message + session extracted from a chat request, or `null` when the body is invalid. */\nexport interface AgentRequestInput {\n message: string\n sessionId: string\n}\n\n/** Extract the text of a `{ type: 'text', text: string }` part from an untrusted value. */\nfunction partText(part: unknown): string {\n if (typeof part !== 'object' || part === null) return ''\n const p = part as Record<string, unknown>\n return p.type === 'text' && typeof p.text === 'string' ? p.text : ''\n}\n\nfunction jsonError(status: number, code: string, message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/**\n * Extract `{ message, sessionId }` from a chat request body. Returns `null` when neither the\n * ai-sdk `messages` shape nor the simple `message` shape yields non-empty user text.\n */\nexport function parseAgentRequestBody(body: unknown): AgentRequestInput | null {\n if (typeof body !== 'object' || body === null) return null\n const b = body as Record<string, unknown>\n\n // ai-sdk useChat shape: { id, messages: UIMessage[] } — take the last message's text parts.\n if (Array.isArray(b.messages) && b.messages.length > 0) {\n const last = b.messages[b.messages.length - 1] as Record<string, unknown>\n const parts = Array.isArray(last.parts) ? last.parts : []\n const text = parts.map(partText).join('')\n if (text.length === 0) return null\n const sessionId = typeof b.id === 'string' && b.id.length > 0 ? b.id : crypto.randomUUID()\n return { message: text, sessionId }\n }\n\n // Simple shape: { message, sessionId? }.\n if (typeof b.message === 'string' && b.message.length > 0) {\n const sessionId =\n typeof b.sessionId === 'string' && b.sessionId.length > 0 ? b.sessionId : crypto.randomUUID()\n return { message: b.message, sessionId }\n }\n\n return null\n}\n\n/**\n * Mount a loaded agent module as a `Response`. `apiKey` is resolved by the caller\n * (`resolveProvider`). `source` labels a fail-fast `AgentDefinitionError` (the file path).\n */\nexport async function mountAgent(\n mod: unknown,\n request: Request,\n apiKey: string,\n source = 'agent module',\n csrfMode: CsrfMode = 'strict',\n): Promise<Response> {\n // Enforce CSRF BEFORE any work — an agent run spends real LLM tokens, so a cross-origin\n // POST must be rejected before it reaches the SDK (parity with actions/routes). The custom\n // `X-Theo-Action` header + Origin match is the same defense `executeRoute`/`executeAction`\n // apply; the `useAgent` client sends the header. `off` skips; `warn` never blocks.\n if (csrfMode !== 'off') {\n const csrf = validateCsrfRequest(request)\n if (!csrf.valid && csrfMode === 'strict') {\n return jsonError(403, 'CSRF_FAILED', `CSRF check failed: ${csrf.reason}`)\n }\n }\n\n const compiled = compileAgentModule(mod, source)\n\n let body: unknown = null\n try {\n body = await request.json()\n } catch {\n /* invalid/empty JSON → handled below as a 400 */\n }\n\n const input = parseAgentRequestBody(body)\n if (input === null) {\n return jsonError(400, 'BAD_REQUEST', 'Request must contain a non-empty message or messages[].')\n }\n\n // When the agent has @HumanInTheLoop-gated tools (M4), wire the pause: the plugin's `awaitApproval`\n // registers a pending approval in the shared registry (the Promise that PAUSES the run); the\n // approve route (`handleAgentApproval`) resolves it. No gated tools ⇒ the M2 stream path unchanged.\n const gated = compiled.hitl\n const registry = getApprovalRegistry()\n const hitl =\n gated && gated.size > 0\n ? {\n gated,\n awaitApproval: (approvalId: string, opts: HumanInTheLoopOptions) =>\n registry.register(approvalId, {\n timeoutMs: opts.timeout ?? 300_000,\n onTimeout: opts.onTimeout ?? 'abort',\n }),\n }\n : undefined\n\n return uiMessageStreamResponse(\n streamAgentUIMessages(compiled, apiKey, { ...input, hitl, signal: request.signal }),\n )\n}\n","import type { UIMessageChunk } from 'ai'\n\n/**\n * M0 (theokit-ai-first) — serialize a stream of ai-sdk `UIMessageChunk`s into a\n * Web-Standards `Response` on the UIMessageStream wire so `@ai-sdk/react`'s\n * `useChat` consumes it WITHOUT a custom adapter.\n *\n * Wire contract (must match ai-sdk's consumer transport exactly):\n * - `content-type: text/event-stream`\n * - `x-vercel-ai-ui-message-stream: v1` — the version marker `useChat` checks\n * - each chunk framed as `data: ${JSON.stringify(chunk)}\\n\\n`\n * - a terminal `data: [DONE]\\n\\n` after the last chunk (ignored by the parser)\n *\n * Web Standards only — `Response` + `ReadableStream` (G8, no node:http). The\n * headers commit before the stream starts.\n *\n * Fail-clear (error-handling.md): if the source iterable throws mid-stream, the\n * `[DONE]` terminal is still flushed and the stream is closed — never left\n * hanging. (The translator upstream already closes gracefully; this is defense\n * in depth for any other chunk source.)\n */\nconst UI_MESSAGE_STREAM_HEADERS = {\n 'content-type': 'text/event-stream',\n 'x-vercel-ai-ui-message-stream': 'v1',\n} as const\n\nconst DONE_FRAME = 'data: [DONE]\\n\\n'\n\nfunction encode(text: string): Uint8Array {\n return new TextEncoder().encode(text)\n}\n\nexport function uiMessageStreamResponse(chunks: AsyncIterable<UIMessageChunk>): Response {\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n for await (const chunk of chunks) {\n controller.enqueue(encode(`data: ${JSON.stringify(chunk)}\\n\\n`))\n }\n } catch {\n // The source iterable aborted mid-stream. The upstream translator owns\n // error semantics (it surfaces failures as chunks + closes gracefully);\n // this transport's sole guarantee is a terminated stream — fall through\n // to the DONE terminal rather than re-throw and error an open stream.\n } finally {\n controller.enqueue(encode(DONE_FRAME))\n controller.close()\n }\n },\n })\n return new Response(stream, { headers: UI_MESSAGE_STREAM_HEADERS })\n}\n","/**\n * T5a.2 Phase G slice 5/N — Node adapter shim for the Web request handler.\n *\n * Bridges Node's `IncomingMessage` + `ServerResponse` shape to the\n * Web-Standards `executeWebRequest` (Phase A → G slices 1-4). Per\n * ADR-0028 R3a, the Node adapter is the ONLY place IncomingMessage ↔\n * Request conversion happens — every other layer of `server/` flows\n * through Web `Request`/`Response`.\n *\n * **Two conversions:**\n *\n * 1. `incomingMessageToWebRequest(req)` — Node → Web. Reads\n * `req.method`, `req.url`, `req.headers`, and (for POST/PUT/etc.)\n * drains the Node Readable body into a Web `ReadableStream`. The\n * request URL is resolved to absolute form using `req.headers.host`\n * (Web Request guarantees absolute URL).\n *\n * 2. `writeWebResponseToServerResponse(response, res)` — Web → Node.\n * Sets status code + status text, copies headers (including all\n * `Set-Cookie` values via `getSetCookie()`), then drains the Web\n * `ReadableStream` body into the Node ServerResponse.\n *\n * Plus the convenience composer `executeWebRequestFromNode(req, res,\n * routeModule, opts?)` that wires both ends — exactly what api-middleware\n * (and the prod CLI start path) need to migrate from the legacy\n * `executeRoute` to `executeWebRequest` without touching call sites.\n *\n * Per `docs/plans/t5a2-incoming-message-to-request-shape-refactor-plan.md`\n * v1.0 § Phase G slice 5/N (closes the executor bridge surface).\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { Readable } from 'node:stream'\n\nimport { executeWebRequest, type ExecuteWebRequestOptions } from '../web-handler.js'\n\n/**\n * Build a Web `Request` from a Node `IncomingMessage`. The Web Request\n * spec requires an absolute URL; we synthesize one from\n * `req.headers.host` (or fall back to `localhost` for test doubles).\n *\n * For methods with a body (POST/PUT/PATCH/DELETE), the Node Readable\n * stream is wrapped as a Web ReadableStream via `Readable.toWeb()` so\n * downstream consumers can call `request.json()` / `request.formData()`\n * / `request.text()` natively.\n *\n * EC-1: Node sometimes provides headers as `string | string[]`. Web\n * `Headers` collapses to single-comma-joined strings — we do the join\n * manually for repeated headers because `Headers.append` would create\n * multi-value entries which behave differently on `.get()`.\n */\nexport function incomingMessageToWebRequest(req: IncomingMessage): Request {\n const host = pickHeaderString(req.headers.host) ?? 'localhost'\n const url = `http://${host}${req.url ?? '/'}`\n\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n headers.set(key, value.join(', '))\n } else {\n headers.set(key, value)\n }\n }\n\n const method = (req.method ?? 'GET').toUpperCase()\n const hasBody = method !== 'GET' && method !== 'HEAD'\n\n if (!hasBody) {\n return new Request(url, { method, headers })\n }\n\n // Drain Node's Readable into a Web ReadableStream. `Readable.toWeb` is\n // available in Node 18+ (theokit's engines.node floor is 22+, so safe).\n const webStream = Readable.toWeb(req) as ReadableStream\n return new Request(url, {\n method,\n headers,\n body: webStream,\n // EC-2: Node 18+ requires `duplex: 'half'` when body is a stream.\n // The `RequestInit` type omits it (Web spec gap); cast accordingly.\n ...({ duplex: 'half' } as { duplex: 'half' }),\n })\n}\n\n/**\n * Write a Web `Response` into a Node `ServerResponse`. Mirrors the Node\n * `res.writeHead` + `res.end` pattern.\n *\n * Set-Cookie is the only multi-value header the Web spec exposes via\n * `getSetCookie()`. We append each entry individually so Node emits\n * separate `Set-Cookie:` lines per the HTTP spec.\n *\n * If the Response body is a `ReadableStream`, it's piped chunk-by-chunk\n * to `res`. Empty body (null) → just close.\n */\nexport async function writeWebResponseToServerResponse(\n response: Response,\n res: ServerResponse,\n): Promise<void> {\n // Status + headers FIRST (writeHead locks them).\n // EC-3: Set-Cookie needs special handling (writeHead's plain object\n // shape conflicts with multi-value headers; we set them via setHeader\n // BEFORE writeHead so the array form is preserved).\n const setCookies = response.headers.getSetCookie()\n if (setCookies.length > 0) {\n res.setHeader('Set-Cookie', setCookies)\n }\n const otherHeaders: Record<string, string> = {}\n for (const [key, value] of response.headers.entries()) {\n if (key.toLowerCase() === 'set-cookie') continue\n otherHeaders[key] = value\n }\n res.writeHead(response.status, response.statusText, otherHeaders)\n\n // Body — drain ReadableStream OR just end() for null body.\n if (response.body === null) {\n res.end()\n return\n }\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n // Node's res.write accepts Uint8Array natively (no conversion needed).\n res.write(value)\n }\n res.end()\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Convenience composer — full request lifecycle Node → Web → Node.\n *\n * Use case: existing api-middleware (and the prod CLI start path)\n * receive Node `(req, res)` from `http.createServer`. To migrate to the\n * Web-Standards executor without rewriting every call site, wrap with\n * this composer:\n *\n * import * as users from './app/users/route.js'\n * await executeWebRequestFromNode(req, res, users, { csrfMode: 'strict' })\n *\n * The Web request is built, dispatched through executeWebRequest, and\n * the Response is drained back into `res`. Caller does NOT need to call\n * `res.end()` afterwards — this composer handles it.\n */\nexport async function executeWebRequestFromNode(\n req: IncomingMessage,\n res: ServerResponse,\n routeModule: Parameters<typeof executeWebRequest>[1],\n opts?: ExecuteWebRequestOptions,\n): Promise<void> {\n const webRequest = incomingMessageToWebRequest(req)\n const webResponse = await executeWebRequest(webRequest, routeModule, opts)\n await writeWebResponseToServerResponse(webResponse, res)\n}\n\n/** Pick the first usable string from Node's `string | string[] | undefined` headers. */\nfunction pickHeaderString(value: string | string[] | undefined): string | undefined {\n if (typeof value === 'string') return value\n if (Array.isArray(value)) {\n for (const v of value) if (typeof v === 'string' && v.length > 0) return v\n }\n return undefined\n}\n"],"mappings":"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","names":["jsonError"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "tsx/esm";
|
|
3
|
+
|
|
4
|
+
// src/server/_internal/scan-walker.ts
|
|
5
|
+
import { readdirSync } from "fs";
|
|
6
|
+
import { extname, join, resolve } from "path";
|
|
7
|
+
function walkSourceFiles(root, opts, onFile) {
|
|
8
|
+
const skipPrefixes = opts.skipPrefixes ?? ["_", "."];
|
|
9
|
+
const visit = (dir) => {
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {
|
|
19
|
+
visit(fullPath);
|
|
20
|
+
} else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {
|
|
21
|
+
onFile(resolve(fullPath));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
visit(root);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
walkSourceFiles
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-P37RZRFV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/_internal/scan-walker.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks directories derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { readdirSync } from 'node:fs'\nimport { extname, join, resolve } from 'node:path'\n\n/**\n * Options for walkSourceFiles.\n *\n * Sequential by design — callers (route precedence) depend on insertion\n * order (EC-19 documented decision). Async-callback support is implicit\n * via JavaScript's serial await loop.\n *\n * Tested on macOS + Linux. Windows long-path support not validated (EC-20).\n */\nexport interface WalkOptions {\n /** File extensions to include (e.g., new Set(['.ts', '.tsx'])). */\n extensions: ReadonlySet<string>\n /** Skip directories whose name starts with any of these (default: ['_', '.']). */\n skipPrefixes?: readonly string[]\n}\n\n/**\n * Recursively walk `root`, invoking `onFile(absPath)` for every file matching\n * `opts.extensions`. Directories whose name starts with any `skipPrefixes`\n * char are skipped (defaults to `_` and `.`).\n *\n * Replaces 3 near-identical recursive walkers in scan.ts, action-scan.ts,\n * ws-scan.ts (PV-3 — DRY consolidation). Resolves T3.1 of\n * architecture-review-remediation-plan.\n *\n * Symlink loops are NOT tracked — callers must avoid them or pre-resolve\n * via `fs.realpath`. EC-11 documented but not implemented (rare in dev).\n */\nexport function walkSourceFiles(\n root: string,\n opts: WalkOptions,\n onFile: (absPath: string) => void,\n): void {\n const skipPrefixes = opts.skipPrefixes ?? ['_', '.']\n const visit = (dir: string): void => {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n // Silently skip unreadable directories — caller controls discoverability\n return\n }\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {\n visit(fullPath)\n } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {\n onFile(resolve(fullPath))\n }\n }\n }\n visit(root)\n}\n"],"mappings":";;;;AAIA,SAAS,mBAAmB;AAC5B,SAAS,SAAS,MAAM,eAAe;AA8BhC,SAAS,gBACd,MACA,MACA,QACM;AACN,QAAM,eAAe,KAAK,gBAAgB,CAAC,KAAK,GAAG;AACnD,QAAM,QAAQ,CAAC,QAAsB;AACnC,QAAI;AACJ,QAAI;AACF,gBAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,IACpD,QAAQ;AAEN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,WAAW,KAAK,KAAK,MAAM,IAAI;AACrC,UAAI,MAAM,YAAY,KAAK,CAAC,aAAa,KAAK,CAAC,MAAM,MAAM,KAAK,WAAW,CAAC,CAAC,GAAG;AAC9E,cAAM,QAAQ;AAAA,MAChB,WAAW,MAAM,OAAO,KAAK,KAAK,WAAW,IAAI,QAAQ,MAAM,IAAI,CAAC,GAAG;AACrE,eAAO,QAAQ,QAAQ,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,QAAM,IAAI;AACZ;","names":[]}
|
|
@@ -984,14 +984,102 @@ function createActionMiddleware(vite, serverDir, options) {
|
|
|
984
984
|
// src/vite-plugin/agent-middleware.ts
|
|
985
985
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
986
986
|
|
|
987
|
+
// src/server/agent/approval-registry.ts
|
|
988
|
+
var serverRegistry;
|
|
989
|
+
function getApprovalRegistry() {
|
|
990
|
+
serverRegistry ??= createInProcessApprovalRegistry();
|
|
991
|
+
return serverRegistry;
|
|
992
|
+
}
|
|
993
|
+
function createInProcessApprovalRegistry() {
|
|
994
|
+
const pending = /* @__PURE__ */ new Map();
|
|
995
|
+
return {
|
|
996
|
+
register(approvalId, opts) {
|
|
997
|
+
return new Promise((resolve10) => {
|
|
998
|
+
const settle = (approved) => {
|
|
999
|
+
const entry = pending.get(approvalId);
|
|
1000
|
+
if (!entry) return;
|
|
1001
|
+
clearTimeout(entry.timer);
|
|
1002
|
+
pending.delete(approvalId);
|
|
1003
|
+
resolve10(approved);
|
|
1004
|
+
};
|
|
1005
|
+
const timer = setTimeout(() => {
|
|
1006
|
+
settle(opts.onTimeout === "proceed");
|
|
1007
|
+
}, opts.timeoutMs);
|
|
1008
|
+
pending.set(approvalId, { settle, timer });
|
|
1009
|
+
});
|
|
1010
|
+
},
|
|
1011
|
+
resolve(approvalId, approved) {
|
|
1012
|
+
const entry = pending.get(approvalId);
|
|
1013
|
+
if (!entry) return false;
|
|
1014
|
+
entry.settle(approved);
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/server/agent/approve-agent.ts
|
|
1021
|
+
var APPROVE_SEGMENT = "/approve/";
|
|
1022
|
+
function parseApprovalId(urlPath) {
|
|
1023
|
+
const at = urlPath.indexOf(APPROVE_SEGMENT);
|
|
1024
|
+
if (at === -1) return null;
|
|
1025
|
+
const id = urlPath.slice(at + APPROVE_SEGMENT.length);
|
|
1026
|
+
return id.length > 0 && !id.includes("/") ? id : null;
|
|
1027
|
+
}
|
|
1028
|
+
function isApprovalPath(urlPath) {
|
|
1029
|
+
return urlPath.includes(APPROVE_SEGMENT);
|
|
1030
|
+
}
|
|
1031
|
+
function jsonError(status, code, message) {
|
|
1032
|
+
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
1033
|
+
status,
|
|
1034
|
+
headers: { "content-type": "application/json" }
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
function parseApprovalBody(body) {
|
|
1038
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1039
|
+
const b = body;
|
|
1040
|
+
return typeof b.approved === "boolean" ? { approved: b.approved } : null;
|
|
1041
|
+
}
|
|
1042
|
+
async function handleAgentApproval(request, urlPath, registry2, csrfMode = "strict") {
|
|
1043
|
+
if (csrfMode !== "off") {
|
|
1044
|
+
const csrf = validateCsrfRequest(request);
|
|
1045
|
+
if (!csrf.valid && csrfMode === "strict") {
|
|
1046
|
+
return jsonError(403, "CSRF_FAILED", `CSRF check failed: ${csrf.reason}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const approvalId = parseApprovalId(urlPath);
|
|
1050
|
+
if (approvalId === null) {
|
|
1051
|
+
return jsonError(400, "BAD_REQUEST", "Approval path must be /api/agents/<name>/approve/<id>.");
|
|
1052
|
+
}
|
|
1053
|
+
let body = null;
|
|
1054
|
+
try {
|
|
1055
|
+
body = await request.json();
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
const parsed = parseApprovalBody(body);
|
|
1059
|
+
if (parsed === null) {
|
|
1060
|
+
return jsonError(400, "BAD_REQUEST", "Request body must contain a boolean `approved`.");
|
|
1061
|
+
}
|
|
1062
|
+
const resolved = registry2.resolve(approvalId, parsed.approved);
|
|
1063
|
+
if (!resolved) {
|
|
1064
|
+
return jsonError(404, "NOT_PENDING", `No pending approval for id '${approvalId}'.`);
|
|
1065
|
+
}
|
|
1066
|
+
return new Response(JSON.stringify({ resolved: true }), {
|
|
1067
|
+
status: 200,
|
|
1068
|
+
headers: { "content-type": "application/json" }
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
987
1072
|
// src/server/agent/mount-agent.ts
|
|
988
|
-
import {
|
|
1073
|
+
import {
|
|
1074
|
+
compileAgentModule,
|
|
1075
|
+
streamAgentUIMessages
|
|
1076
|
+
} from "@theokit/agents";
|
|
989
1077
|
function partText(part) {
|
|
990
1078
|
if (typeof part !== "object" || part === null) return "";
|
|
991
1079
|
const p = part;
|
|
992
1080
|
return p.type === "text" && typeof p.text === "string" ? p.text : "";
|
|
993
1081
|
}
|
|
994
|
-
function
|
|
1082
|
+
function jsonError2(status, code, message) {
|
|
995
1083
|
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
996
1084
|
status,
|
|
997
1085
|
headers: { "content-type": "application/json" }
|
|
@@ -1018,7 +1106,7 @@ async function mountAgent(mod, request, apiKey, source = "agent module", csrfMod
|
|
|
1018
1106
|
if (csrfMode !== "off") {
|
|
1019
1107
|
const csrf = validateCsrfRequest(request);
|
|
1020
1108
|
if (!csrf.valid && csrfMode === "strict") {
|
|
1021
|
-
return
|
|
1109
|
+
return jsonError2(403, "CSRF_FAILED", `CSRF check failed: ${csrf.reason}`);
|
|
1022
1110
|
}
|
|
1023
1111
|
}
|
|
1024
1112
|
const compiled = compileAgentModule(mod, source);
|
|
@@ -1029,9 +1117,20 @@ async function mountAgent(mod, request, apiKey, source = "agent module", csrfMod
|
|
|
1029
1117
|
}
|
|
1030
1118
|
const input = parseAgentRequestBody(body);
|
|
1031
1119
|
if (input === null) {
|
|
1032
|
-
return
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1120
|
+
return jsonError2(400, "BAD_REQUEST", "Request must contain a non-empty message or messages[].");
|
|
1121
|
+
}
|
|
1122
|
+
const gated = compiled.hitl;
|
|
1123
|
+
const registry2 = getApprovalRegistry();
|
|
1124
|
+
const hitl = gated && gated.size > 0 ? {
|
|
1125
|
+
gated,
|
|
1126
|
+
awaitApproval: (approvalId, opts) => registry2.register(approvalId, {
|
|
1127
|
+
timeoutMs: opts.timeout ?? 3e5,
|
|
1128
|
+
onTimeout: opts.onTimeout ?? "abort"
|
|
1129
|
+
})
|
|
1130
|
+
} : void 0;
|
|
1131
|
+
return uiMessageStreamResponse(
|
|
1132
|
+
streamAgentUIMessages(compiled, apiKey, { ...input, hitl, signal: request.signal })
|
|
1133
|
+
);
|
|
1035
1134
|
}
|
|
1036
1135
|
|
|
1037
1136
|
// src/server/agent/provider-resolver.ts
|
|
@@ -1153,6 +1252,42 @@ function createAgentMiddleware(vite, projectRoot, csrfMode = "strict") {
|
|
|
1153
1252
|
const start = Date.now();
|
|
1154
1253
|
res.setHeader("x-request-id", requestId);
|
|
1155
1254
|
const urlPath = url.split("?")[0];
|
|
1255
|
+
if (isApprovalPath(urlPath)) {
|
|
1256
|
+
const method2 = (req.method ?? "POST").toUpperCase();
|
|
1257
|
+
if (method2 !== "POST") {
|
|
1258
|
+
sendError(
|
|
1259
|
+
res,
|
|
1260
|
+
"METHOD_NOT_ALLOWED",
|
|
1261
|
+
"Approve endpoints accept POST",
|
|
1262
|
+
405,
|
|
1263
|
+
void 0,
|
|
1264
|
+
requestId
|
|
1265
|
+
);
|
|
1266
|
+
logRequest({ method: method2, url, status: 405, duration: Date.now() - start, requestId });
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
const request = incomingMessageToWebRequest(req);
|
|
1271
|
+
const response = await handleAgentApproval(
|
|
1272
|
+
request,
|
|
1273
|
+
urlPath,
|
|
1274
|
+
getApprovalRegistry(),
|
|
1275
|
+
csrfMode
|
|
1276
|
+
);
|
|
1277
|
+
await writeWebResponseToServerResponse(response, res);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
sendError(
|
|
1280
|
+
res,
|
|
1281
|
+
"INTERNAL",
|
|
1282
|
+
err instanceof Error ? err.message : "Approve handler failed",
|
|
1283
|
+
500,
|
|
1284
|
+
void 0,
|
|
1285
|
+
requestId
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
logRequest({ method: method2, url, status: res.statusCode, duration: Date.now() - start, requestId });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1156
1291
|
const agent = scanAgents(projectRoot).find((a) => a.agentPath === urlPath);
|
|
1157
1292
|
if (!agent) {
|
|
1158
1293
|
next();
|
|
@@ -2743,4 +2878,4 @@ export {
|
|
|
2743
2878
|
theoPluginAsync,
|
|
2744
2879
|
theoPlugin
|
|
2745
2880
|
};
|
|
2746
|
-
//# sourceMappingURL=chunk-
|
|
2881
|
+
//# sourceMappingURL=chunk-UOR6JTCI.js.map
|