poe-code 4.0.39 → 4.0.41
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/cli/commands/gaslight.js +21 -3
- package/dist/cli/commands/gaslight.js.map +1 -1
- package/dist/index.js +22 -5
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/agent-gaslight/dist/run.js +4 -1
- package/packages/agent-gaslight/dist/types.d.ts +2 -0
- package/packages/toolcraft-openapi/dist/generate.js +20 -3
- package/packages/toolcraft-openapi/dist/http.d.ts +2 -0
- package/packages/toolcraft-openapi/dist/http.js +172 -13
- package/packages/toolcraft-openapi/dist/runtime.js +2 -1
package/package.json
CHANGED
|
@@ -111,6 +111,7 @@ function resolvePlanPaths(options, cwd, homeDir) {
|
|
|
111
111
|
return planPaths;
|
|
112
112
|
}
|
|
113
113
|
export async function runGaslight(options) {
|
|
114
|
+
const startedAt = Date.now();
|
|
114
115
|
const cwd = options.cwd ?? process.cwd();
|
|
115
116
|
const homeDir = options.homeDir ?? os.homedir();
|
|
116
117
|
const fs = options.fs ?? nodeFs;
|
|
@@ -138,6 +139,7 @@ export async function runGaslight(options) {
|
|
|
138
139
|
const plans = [];
|
|
139
140
|
let usage;
|
|
140
141
|
for (const [planIndex, planPath] of planPaths.entries()) {
|
|
142
|
+
const planStartedAt = Date.now();
|
|
141
143
|
const prompts = [
|
|
142
144
|
...(config.setup ? [config.setup] : []),
|
|
143
145
|
`${config.prompt} ${planPath}`,
|
|
@@ -216,8 +218,9 @@ export async function runGaslight(options) {
|
|
|
216
218
|
planPath,
|
|
217
219
|
...(archivedPath ? { archivedPath } : {}),
|
|
218
220
|
rounds: planRounds,
|
|
221
|
+
durationMs: Date.now() - planStartedAt,
|
|
219
222
|
...(planUsage ? { usage: planUsage } : {})
|
|
220
223
|
});
|
|
221
224
|
}
|
|
222
|
-
return { rounds, plans, ...(usage ? { usage } : {}) };
|
|
225
|
+
return { rounds, plans, durationMs: Date.now() - startedAt, ...(usage ? { usage } : {}) };
|
|
223
226
|
}
|
|
@@ -34,12 +34,14 @@ export interface GaslightRound {
|
|
|
34
34
|
export interface GaslightResult {
|
|
35
35
|
rounds: GaslightRound[];
|
|
36
36
|
plans: GaslightPlanResult[];
|
|
37
|
+
durationMs: number;
|
|
37
38
|
usage?: SpawnUsage;
|
|
38
39
|
}
|
|
39
40
|
export interface GaslightPlanResult {
|
|
40
41
|
planPath: string;
|
|
41
42
|
archivedPath?: string;
|
|
42
43
|
rounds: GaslightRound[];
|
|
44
|
+
durationMs: number;
|
|
43
45
|
usage?: SpawnUsage;
|
|
44
46
|
}
|
|
45
47
|
export interface GaslightOptions {
|
|
@@ -550,7 +550,12 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
550
550
|
? Object.entries(schema.properties)
|
|
551
551
|
.filter(([, property]) => {
|
|
552
552
|
const resolved = resolveBodySchema(document, property, operationId, "requestBody multipart field");
|
|
553
|
-
|
|
553
|
+
if (resolved.type === "string" && resolved.format === "binary")
|
|
554
|
+
return true;
|
|
555
|
+
if (resolved.type !== "array" || resolved.items === undefined)
|
|
556
|
+
return false;
|
|
557
|
+
const items = resolveBodySchema(document, resolved.items, operationId, "requestBody multipart array item");
|
|
558
|
+
return items.type === "string" && items.format === "binary";
|
|
554
559
|
})
|
|
555
560
|
.map(([name]) => name)
|
|
556
561
|
: undefined;
|
|
@@ -579,10 +584,18 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
579
584
|
const assemblies = [];
|
|
580
585
|
const declaredPropertyCount = Object.keys(schema.properties).length;
|
|
581
586
|
for (const [name, property] of Object.entries(schema.properties)) {
|
|
582
|
-
const
|
|
583
|
-
if (
|
|
587
|
+
const resolvedPropertySchema = resolveBodySchema(document, property, operationId, `requestBody.properties.${name}`);
|
|
588
|
+
if (resolvedPropertySchema.readOnly === true) {
|
|
584
589
|
continue;
|
|
585
590
|
}
|
|
591
|
+
const propertySchema = multipartBinaryFields?.includes(name) === true
|
|
592
|
+
? {
|
|
593
|
+
...resolvedPropertySchema,
|
|
594
|
+
description: resolvedPropertySchema.description === undefined
|
|
595
|
+
? "Local path, HTTP(S) URL, or base64 value."
|
|
596
|
+
: `${resolvedPropertySchema.description} Local path, HTTP(S) URL, or base64 value.`
|
|
597
|
+
}
|
|
598
|
+
: resolvedPropertySchema;
|
|
586
599
|
const generated = createBodyField(document, name, propertySchema, bodyOptional || !required.has(name), operationId);
|
|
587
600
|
assemblies.push(generated);
|
|
588
601
|
}
|
|
@@ -1564,6 +1577,7 @@ function createCommandFile(options) {
|
|
|
1564
1577
|
lines.push(` multipartBinaryFields: ${JSON.stringify(options.multipartBinaryFields)},`);
|
|
1565
1578
|
lines.push(" fs,");
|
|
1566
1579
|
lines.push(" env,");
|
|
1580
|
+
lines.push(" fetch,");
|
|
1567
1581
|
lines.push(" });");
|
|
1568
1582
|
}
|
|
1569
1583
|
else {
|
|
@@ -1627,6 +1641,9 @@ function createCommandFile(options) {
|
|
|
1627
1641
|
if (usesBinaryOutput) {
|
|
1628
1642
|
lines.push(" return writeBinaryResponseOutput(result, params.output, { fs, env });");
|
|
1629
1643
|
}
|
|
1644
|
+
else if (usesRequestShapeVariable) {
|
|
1645
|
+
lines.push(" return result;");
|
|
1646
|
+
}
|
|
1630
1647
|
lines.push(" },");
|
|
1631
1648
|
lines.push("});");
|
|
1632
1649
|
lines.push("");
|
|
@@ -56,6 +56,8 @@ export declare function prepareMultipartFileInputs(requestShape: RequestShape, o
|
|
|
56
56
|
multipartBinaryFields?: readonly string[];
|
|
57
57
|
fs?: RuntimeFileSystem;
|
|
58
58
|
env?: RuntimeEnvironment;
|
|
59
|
+
fetch?: typeof globalThis.fetch;
|
|
60
|
+
signal?: AbortSignal;
|
|
59
61
|
}): Promise<RequestShape>;
|
|
60
62
|
export declare function writeBinaryResponseOutput(result: unknown, outputPath: unknown, runtime: {
|
|
61
63
|
fs?: RuntimeFileSystem;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { text as designText } from "toolcraft-design";
|
|
4
5
|
import { HttpError, UserError, createHttpError, shouldEmitDiagnostic } from "toolcraft";
|
|
@@ -6,6 +7,10 @@ import { classifyNetworkError } from "./network-error.js";
|
|
|
6
7
|
import { redactHeaders, redactHeaderValue, redactSensitiveQueryValues } from "./redaction.js";
|
|
7
8
|
export { HttpError };
|
|
8
9
|
const TRANSCRIPT_BODY_BYTE_LIMIT = 4 * 1024;
|
|
10
|
+
const MULTIPART_FILE_BYTE_LIMIT = 100 * 1024 * 1024;
|
|
11
|
+
const MULTIPART_REQUEST_BYTE_LIMIT = 250 * 1024 * 1024;
|
|
12
|
+
const MULTIPART_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
13
|
+
const MULTIPART_REDIRECT_LIMIT = 5;
|
|
9
14
|
export async function requestJson(options) {
|
|
10
15
|
const token = options.auth === "none" ? undefined : await options.tokenSource.getToken();
|
|
11
16
|
const method = options.method.toUpperCase();
|
|
@@ -214,25 +219,174 @@ export async function prepareMultipartFileInputs(requestShape, options) {
|
|
|
214
219
|
return requestShape;
|
|
215
220
|
}
|
|
216
221
|
const body = { ...requestShape.body };
|
|
222
|
+
const runtime = {
|
|
223
|
+
field: "",
|
|
224
|
+
fs: options.fs,
|
|
225
|
+
env: options.env,
|
|
226
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
227
|
+
signal: options.signal,
|
|
228
|
+
totalBytes: 0
|
|
229
|
+
};
|
|
217
230
|
for (const field of options.multipartBinaryFields) {
|
|
218
231
|
const value = body[field];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
const filePath = resolveUserPath(value, options.env);
|
|
223
|
-
if (!(await options.fs.exists(filePath))) {
|
|
232
|
+
const sources = Array.isArray(value) ? value : [value];
|
|
233
|
+
if (sources.some((source) => typeof source !== "string")) {
|
|
224
234
|
continue;
|
|
225
235
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
236
|
+
runtime.field = field;
|
|
237
|
+
const resolved = await Promise.all(sources.map((source) => resolveMultipartSource(source, runtime)));
|
|
238
|
+
body[field] = Array.isArray(value) ? resolved : resolved[0];
|
|
230
239
|
}
|
|
231
240
|
return {
|
|
232
241
|
...requestShape,
|
|
233
242
|
body
|
|
234
243
|
};
|
|
235
244
|
}
|
|
245
|
+
async function resolveMultipartSource(source, runtime) {
|
|
246
|
+
if (URL.canParse(source) && source.includes(":")) {
|
|
247
|
+
const sourceUrl = new URL(source);
|
|
248
|
+
validateMultipartUrl(sourceUrl, runtime.field);
|
|
249
|
+
return downloadMultipartSource(sourceUrl, runtime);
|
|
250
|
+
}
|
|
251
|
+
const filePath = resolveUserPath(source, runtime.env);
|
|
252
|
+
if (await runtime.fs.exists(filePath)) {
|
|
253
|
+
const data = await runtime.fs.readFile(filePath, "base64");
|
|
254
|
+
accountMultipartBytes(Buffer.byteLength(data, "base64"), source, runtime);
|
|
255
|
+
return {
|
|
256
|
+
data,
|
|
257
|
+
filename: path.basename(filePath) || runtime.field,
|
|
258
|
+
contentType: inferContentType(filePath)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (isValidBase64(source))
|
|
262
|
+
return source;
|
|
263
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(source)} does not exist.`);
|
|
264
|
+
}
|
|
265
|
+
async function downloadMultipartSource(initialUrl, runtime) {
|
|
266
|
+
let url = initialUrl;
|
|
267
|
+
const timeoutSignal = AbortSignal.timeout(MULTIPART_DOWNLOAD_TIMEOUT_MS);
|
|
268
|
+
const signal = runtime.signal === undefined
|
|
269
|
+
? timeoutSignal
|
|
270
|
+
: AbortSignal.any([runtime.signal, timeoutSignal]);
|
|
271
|
+
for (let redirects = 0; redirects <= MULTIPART_REDIRECT_LIMIT; redirects += 1) {
|
|
272
|
+
validateMultipartUrl(url, runtime.field);
|
|
273
|
+
let response;
|
|
274
|
+
try {
|
|
275
|
+
response = await runtime.fetch(url, { redirect: "manual", signal });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
throw new UserError(`Could not download multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(redactMultipartUrl(url))}.`);
|
|
279
|
+
}
|
|
280
|
+
if (response.status >= 300 && response.status < 400) {
|
|
281
|
+
const location = response.headers.get("location");
|
|
282
|
+
if (location === null || redirects === MULTIPART_REDIRECT_LIMIT) {
|
|
283
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(redactMultipartUrl(initialUrl))} exceeded the redirect limit.`);
|
|
284
|
+
}
|
|
285
|
+
url = new URL(location, url);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(redactMultipartUrl(url))} returned HTTP ${response.status}.`);
|
|
290
|
+
}
|
|
291
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
292
|
+
if (Number.isFinite(declaredLength) && declaredLength > MULTIPART_FILE_BYTE_LIMIT) {
|
|
293
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(redactMultipartUrl(url))} exceeds the 100 MiB file limit.`);
|
|
294
|
+
}
|
|
295
|
+
const bytes = await readMultipartResponse(response, url, runtime);
|
|
296
|
+
accountMultipartBytes(bytes.byteLength, redactMultipartUrl(url), runtime);
|
|
297
|
+
return {
|
|
298
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
299
|
+
filename: selectRemoteFilename(response, url, runtime.field),
|
|
300
|
+
contentType: normalizeContentType(response.headers.get("content-type"))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
throw new UserError("Unexpected multipart redirect state.");
|
|
304
|
+
}
|
|
305
|
+
async function readMultipartResponse(response, url, runtime) {
|
|
306
|
+
if (response.body === null)
|
|
307
|
+
return new Uint8Array();
|
|
308
|
+
const reader = response.body.getReader();
|
|
309
|
+
const chunks = [];
|
|
310
|
+
let byteLength = 0;
|
|
311
|
+
while (true) {
|
|
312
|
+
const { done, value } = await reader.read();
|
|
313
|
+
if (done)
|
|
314
|
+
break;
|
|
315
|
+
byteLength += value.byteLength;
|
|
316
|
+
if (byteLength > MULTIPART_FILE_BYTE_LIMIT) {
|
|
317
|
+
await reader.cancel();
|
|
318
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(redactMultipartUrl(url))} exceeds the 100 MiB file limit.`);
|
|
319
|
+
}
|
|
320
|
+
chunks.push(value);
|
|
321
|
+
}
|
|
322
|
+
const result = new Uint8Array(byteLength);
|
|
323
|
+
let offset = 0;
|
|
324
|
+
for (const chunk of chunks) {
|
|
325
|
+
result.set(chunk, offset);
|
|
326
|
+
offset += chunk.byteLength;
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
function validateMultipartUrl(url, field) {
|
|
331
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) {
|
|
332
|
+
throw new UserError(`Multipart field ${JSON.stringify(field)} uses a disallowed URL.`);
|
|
333
|
+
}
|
|
334
|
+
const hostname = url.hostname.toLowerCase();
|
|
335
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || isPrivateIpLiteral(hostname)) {
|
|
336
|
+
throw new UserError(`Multipart field ${JSON.stringify(field)} uses a private network URL.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function isPrivateIpLiteral(hostname) {
|
|
340
|
+
const normalized = hostname.startsWith("[") && hostname.endsWith("]")
|
|
341
|
+
? hostname.slice(1, -1)
|
|
342
|
+
: hostname;
|
|
343
|
+
const version = isIP(normalized);
|
|
344
|
+
if (version === 4) {
|
|
345
|
+
const octets = normalized.split(".").map(Number);
|
|
346
|
+
return octets[0] === 10 || octets[0] === 127 || octets[0] === 0 ||
|
|
347
|
+
(octets[0] === 169 && octets[1] === 254) ||
|
|
348
|
+
(octets[0] === 172 && (octets[1] ?? 0) >= 16 && (octets[1] ?? 0) <= 31) ||
|
|
349
|
+
(octets[0] === 192 && octets[1] === 168);
|
|
350
|
+
}
|
|
351
|
+
return version === 6 && (normalized === "::1" || normalized.startsWith("fe80:") || normalized.startsWith("fc") || normalized.startsWith("fd"));
|
|
352
|
+
}
|
|
353
|
+
function accountMultipartBytes(bytes, source, runtime) {
|
|
354
|
+
if (bytes > MULTIPART_FILE_BYTE_LIMIT) {
|
|
355
|
+
throw new UserError(`Multipart field ${JSON.stringify(runtime.field)} source ${JSON.stringify(source)} exceeds the 100 MiB file limit.`);
|
|
356
|
+
}
|
|
357
|
+
runtime.totalBytes += bytes;
|
|
358
|
+
if (runtime.totalBytes > MULTIPART_REQUEST_BYTE_LIMIT) {
|
|
359
|
+
throw new UserError("Multipart file inputs exceed the 250 MiB request limit.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function selectRemoteFilename(response, url, fallback) {
|
|
363
|
+
const disposition = response.headers.get("content-disposition");
|
|
364
|
+
const candidate = disposition?.split(";").map((part) => part.trim()).find((part) => part.toLowerCase().startsWith("filename="))?.slice("filename=".length).replaceAll('"', "");
|
|
365
|
+
return sanitizeFilename(candidate ?? path.posix.basename(url.pathname) ?? fallback, fallback);
|
|
366
|
+
}
|
|
367
|
+
function sanitizeFilename(value, fallback) {
|
|
368
|
+
const basename = path.basename(value.replaceAll("\\", "/"));
|
|
369
|
+
const cleaned = Array.from(basename).filter((character) => character >= " " && character !== "\u007f").join("").trim();
|
|
370
|
+
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
371
|
+
}
|
|
372
|
+
function normalizeContentType(value) {
|
|
373
|
+
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
374
|
+
return mediaType?.includes("/") === true ? mediaType : "application/octet-stream";
|
|
375
|
+
}
|
|
376
|
+
function inferContentType(filePath) {
|
|
377
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
378
|
+
return {
|
|
379
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
|
|
380
|
+
".webp": "image/webp", ".pdf": "application/pdf", ".txt": "text/plain", ".json": "application/json",
|
|
381
|
+
".wav": "audio/wav", ".mp3": "audio/mpeg", ".mp4": "video/mp4"
|
|
382
|
+
}[extension] ?? "application/octet-stream";
|
|
383
|
+
}
|
|
384
|
+
function redactMultipartUrl(url) {
|
|
385
|
+
const redacted = new URL(url);
|
|
386
|
+
redacted.username = "";
|
|
387
|
+
redacted.password = "";
|
|
388
|
+
return redactSensitiveQueryValues(redacted.toString());
|
|
389
|
+
}
|
|
236
390
|
export async function writeBinaryResponseOutput(result, outputPath, runtime) {
|
|
237
391
|
if (outputPath === undefined) {
|
|
238
392
|
return result;
|
|
@@ -375,14 +529,16 @@ function decodeMultipartBinaryValue(value, fallbackFilename) {
|
|
|
375
529
|
if (typeof value === "string") {
|
|
376
530
|
return {
|
|
377
531
|
data: value,
|
|
378
|
-
filename: fallbackFilename
|
|
532
|
+
filename: fallbackFilename,
|
|
533
|
+
contentType: "application/octet-stream"
|
|
379
534
|
};
|
|
380
535
|
}
|
|
381
536
|
if (value !== null &&
|
|
382
537
|
typeof value === "object" &&
|
|
383
538
|
!Array.isArray(value) &&
|
|
384
539
|
typeof value.data === "string" &&
|
|
385
|
-
typeof value.filename === "string"
|
|
540
|
+
typeof value.filename === "string" &&
|
|
541
|
+
typeof value.contentType === "string") {
|
|
386
542
|
return value;
|
|
387
543
|
}
|
|
388
544
|
throw new UserError("Multipart binary request fields must be base64 strings or resolved file inputs.");
|
|
@@ -397,8 +553,11 @@ function serializeMultipartBody(body, binaryFields = []) {
|
|
|
397
553
|
if (value === undefined)
|
|
398
554
|
continue;
|
|
399
555
|
if (binary.has(key)) {
|
|
400
|
-
const
|
|
401
|
-
|
|
556
|
+
const values = Array.isArray(value) ? value : [value];
|
|
557
|
+
for (const item of values) {
|
|
558
|
+
const file = decodeMultipartBinaryValue(item, key);
|
|
559
|
+
form.append(key, new Blob([decodeBase64Body(file.data)], { type: file.contentType }), file.filename);
|
|
560
|
+
}
|
|
402
561
|
}
|
|
403
562
|
else if (Array.isArray(value)) {
|
|
404
563
|
for (const item of value)
|
|
@@ -134,7 +134,8 @@ function createRuntimeHandler(command) {
|
|
|
134
134
|
bodyMode: command.bodyMode,
|
|
135
135
|
multipartBinaryFields: command.multipartBinaryFields,
|
|
136
136
|
fs,
|
|
137
|
-
env
|
|
137
|
+
env,
|
|
138
|
+
fetch
|
|
138
139
|
});
|
|
139
140
|
const result = await requestJson({
|
|
140
141
|
baseUrl: command.baseUrl ?? baseUrl,
|