create-loopengine 0.1.45 → 0.1.46
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/package.json +1 -1
- package/template/adapters/http.ts +88 -2
package/package.json
CHANGED
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
// file's call — see AgentConfig.sessionIdFor and defaultSessionIdFor below.
|
|
30
30
|
import { execFile } from 'node:child_process'
|
|
31
31
|
import { randomUUID, timingSafeEqual } from 'node:crypto'
|
|
32
|
-
import { existsSync } from 'node:fs'
|
|
32
|
+
import { createReadStream, existsSync } from 'node:fs'
|
|
33
|
+
import { stat } from 'node:fs/promises'
|
|
33
34
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
34
|
-
import { join } from 'node:path'
|
|
35
|
+
import { extname, join, resolve as resolvePath, sep } from 'node:path'
|
|
35
36
|
import { pathToFileURL } from 'node:url'
|
|
36
37
|
import { promisify } from 'node:util'
|
|
37
38
|
import { getEntry, listAgents, projectDir, registerAgent, updateAgent, type RegistryEntry } from '../agent-registry.js'
|
|
@@ -2109,6 +2110,86 @@ async function handleStorageRedirect(req: IncomingMessage, res: ServerResponse):
|
|
|
2109
2110
|
}
|
|
2110
2111
|
}
|
|
2111
2112
|
|
|
2113
|
+
const LOCAL_FILE_CONTENT_TYPES: Record<string, string> = {
|
|
2114
|
+
'.png': 'image/png',
|
|
2115
|
+
'.jpg': 'image/jpeg',
|
|
2116
|
+
'.jpeg': 'image/jpeg',
|
|
2117
|
+
'.webp': 'image/webp',
|
|
2118
|
+
'.gif': 'image/gif',
|
|
2119
|
+
'.zip': 'application/zip',
|
|
2120
|
+
'.pdf': 'application/pdf',
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// storage-redirect above only ever has a URL to redirect to *because*
|
|
2124
|
+
// the cloud provider already serves the bytes itself — AD_IMAGE_STORAGE=
|
|
2125
|
+
// local/ARCHIVE_STORAGE=local has no such external server at all, so
|
|
2126
|
+
// this route serves the bytes directly instead of redirecting anywhere,
|
|
2127
|
+
// giving local storage the same preview/download-button treatment
|
|
2128
|
+
// storage-redirect already gives gcs, instead of the bare filesystem
|
|
2129
|
+
// path a local ability result used to be stuck with (unopenable by
|
|
2130
|
+
// anything except direct server access).
|
|
2131
|
+
//
|
|
2132
|
+
// `path` is resolved against process.cwd() (this process's own working
|
|
2133
|
+
// directory — not this file's location, and not any one ability's own
|
|
2134
|
+
// AD_IMAGE_OUTPUT_DIR/ARCHIVE_OUTPUT_DIR specifically, since this route
|
|
2135
|
+
// has no idea which ability or env var produced the value it's given)
|
|
2136
|
+
// and then checked against that same root before anything is read —
|
|
2137
|
+
// this route is otherwise a generic "serve a file from this
|
|
2138
|
+
// deployment's own project directory," and skipping that check would
|
|
2139
|
+
// turn it into an arbitrary-file-read over HTTP. AD_IMAGE_OUTPUT_DIR/
|
|
2140
|
+
// ARCHIVE_OUTPUT_DIR left at their own defaults (a relative path) are
|
|
2141
|
+
// already under cwd, so this covers the common case with zero
|
|
2142
|
+
// configuration; a deployment pointing either at an absolute path
|
|
2143
|
+
// outside the project directory gets no preview/download URL for that
|
|
2144
|
+
// file at all — see generate_google_ad_images.ts's/create_zip_archive.ts's
|
|
2145
|
+
// own saveImage/saveArchive doc comments for that fallback.
|
|
2146
|
+
//
|
|
2147
|
+
// Same Basic Auth gate as every other route on this server — see
|
|
2148
|
+
// handleStorageRedirect's own doc comment for why that's what makes a
|
|
2149
|
+
// plain <img src="/local-file?..."> just work with no extra wiring.
|
|
2150
|
+
async function handleLocalFile(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
2151
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
2152
|
+
const relativePath = url.searchParams.get('path')
|
|
2153
|
+
const disposition = url.searchParams.get('disposition')
|
|
2154
|
+
const filename = url.searchParams.get('filename')
|
|
2155
|
+
|
|
2156
|
+
if (!relativePath) {
|
|
2157
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'local-file requires a path query param' }))
|
|
2158
|
+
return
|
|
2159
|
+
}
|
|
2160
|
+
if (disposition && disposition !== 'inline' && disposition !== 'attachment') {
|
|
2161
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `disposition must be "inline" or "attachment" — got "${disposition}"` }))
|
|
2162
|
+
return
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const root = process.cwd()
|
|
2166
|
+
const resolved = resolvePath(root, relativePath)
|
|
2167
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
2168
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'local-file: path escapes this deployment\'s own project directory' }))
|
|
2169
|
+
return
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
let stats: Awaited<ReturnType<typeof stat>>
|
|
2173
|
+
try {
|
|
2174
|
+
stats = await stat(resolved)
|
|
2175
|
+
} catch {
|
|
2176
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `local-file: no such file "${relativePath}"` }))
|
|
2177
|
+
return
|
|
2178
|
+
}
|
|
2179
|
+
if (!stats.isFile()) {
|
|
2180
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `local-file: "${relativePath}" is not a regular file` }))
|
|
2181
|
+
return
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
const contentType = LOCAL_FILE_CONTENT_TYPES[extname(resolved).toLowerCase()] ?? 'application/octet-stream'
|
|
2185
|
+
const headers: Record<string, string> = { 'content-type': contentType, 'content-length': String(stats.size) }
|
|
2186
|
+
if (disposition === 'attachment') {
|
|
2187
|
+
headers['content-disposition'] = `attachment; filename="${filename || relativePath.split('/').pop()}"`
|
|
2188
|
+
}
|
|
2189
|
+
res.writeHead(200, headers)
|
|
2190
|
+
createReadStream(resolved).pipe(res)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2112
2193
|
const server = createServer(async (req, res) => {
|
|
2113
2194
|
if (!isAuthorized(req)) {
|
|
2114
2195
|
res
|
|
@@ -2560,6 +2641,11 @@ const server = createServer(async (req, res) => {
|
|
|
2560
2641
|
return
|
|
2561
2642
|
}
|
|
2562
2643
|
|
|
2644
|
+
if (req.method === 'GET' && pathname === '/local-file') {
|
|
2645
|
+
await handleLocalFile(req, res)
|
|
2646
|
+
return
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2563
2649
|
const match = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/messages$/)
|
|
2564
2650
|
if (!match) {
|
|
2565
2651
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'not found' }))
|