create-loopengine 0.1.43 → 0.1.45
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 +76 -0
package/package.json
CHANGED
|
@@ -137,6 +137,7 @@ import {
|
|
|
137
137
|
import { SkillGarden } from 'loopengine'
|
|
138
138
|
import type { Decision, PendingApproval } from 'actauth'
|
|
139
139
|
import type { LoopEvent } from 'loopengine'
|
|
140
|
+
import { storageSigners } from 'loopengine'
|
|
140
141
|
|
|
141
142
|
const sessions = createSessionStore()
|
|
142
143
|
const checkpoints = createCheckpointStore()
|
|
@@ -2038,6 +2039,76 @@ async function respondAfterResolution(res: ServerResponse, finalCheckpoint: Turn
|
|
|
2038
2039
|
)
|
|
2039
2040
|
}
|
|
2040
2041
|
|
|
2042
|
+
// A generated image/archive's own signed cloud-storage URL is a long,
|
|
2043
|
+
// high-entropy string (a GCS V4 Signature can run ~300 base64
|
|
2044
|
+
// characters) that an agent's own reply has to reproduce character-for-
|
|
2045
|
+
// character to embed as a markdown image/link — expensive in output
|
|
2046
|
+
// tokens, slow to generate, and one flipped character anywhere in it
|
|
2047
|
+
// breaks the whole thing. This route lets a tool return a short, stable
|
|
2048
|
+
// object reference instead — bucket + object name, both human-readable,
|
|
2049
|
+
// not random — and defer the actual signing to request time, right
|
|
2050
|
+
// here, freshly, on every click: shorter for the model to write,
|
|
2051
|
+
// nothing random left to transcribe wrong, and a much shorter
|
|
2052
|
+
// signed-URL lifetime than an ability calling getSignedUrl itself at
|
|
2053
|
+
// generation time ever needed (this one only has to stay valid for as
|
|
2054
|
+
// long as the click that requested it takes to resolve, not however
|
|
2055
|
+
// long a chat reply might sit around unopened).
|
|
2056
|
+
//
|
|
2057
|
+
// Deliberately generic on two axes, not tied to any one ability or
|
|
2058
|
+
// cloud provider: any ability can point a tool's own returned URL here
|
|
2059
|
+
// instead of signing itself, as long as it authenticates the same way
|
|
2060
|
+
// loopengine's own storageSigners.gcs already does
|
|
2061
|
+
// (GOOGLE_APPLICATION_CREDENTIALS_JSON, falling back to Application
|
|
2062
|
+
// Default Credentials) — that's the same logic
|
|
2063
|
+
// lp-product-ad-images'/lp-file-archiver's own buildGcsStorageClient
|
|
2064
|
+
// already duplicate for the *upload* side, centralized in loopengine
|
|
2065
|
+
// core for signing instead. `provider` picks which one of
|
|
2066
|
+
// loopengine's own storageSigners actually does the signing (only
|
|
2067
|
+
// `gcs` exists today) — adding S3/Azure/etc. later is a loopengine
|
|
2068
|
+
// core change, not something this scaffolded file needs to know about.
|
|
2069
|
+
//
|
|
2070
|
+
// Sits behind the same isAuthorized() Basic Auth gate every other route
|
|
2071
|
+
// on this server already requires (checked once, unconditionally,
|
|
2072
|
+
// before any routing below even runs) — not a separately public
|
|
2073
|
+
// endpoint. A browser that already loaded /playground has that same
|
|
2074
|
+
// origin's Basic Auth credentials cached, so a plain
|
|
2075
|
+
// <img src="/storage-redirect?..."> or download link both just work
|
|
2076
|
+
// with no extra wiring; anything without those credentials gets the
|
|
2077
|
+
// same 401 every other route already gives it.
|
|
2078
|
+
async function handleStorageRedirect(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
2079
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
2080
|
+
const provider = url.searchParams.get('provider')
|
|
2081
|
+
const bucket = url.searchParams.get('bucket')
|
|
2082
|
+
const object = url.searchParams.get('object')
|
|
2083
|
+
const disposition = url.searchParams.get('disposition')
|
|
2084
|
+
const filename = url.searchParams.get('filename')
|
|
2085
|
+
|
|
2086
|
+
if (!provider || !bucket || !object) {
|
|
2087
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'storage-redirect requires provider, bucket, and object query params' }))
|
|
2088
|
+
return
|
|
2089
|
+
}
|
|
2090
|
+
if (disposition && disposition !== 'inline' && disposition !== 'attachment') {
|
|
2091
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `disposition must be "inline" or "attachment" — got "${disposition}"` }))
|
|
2092
|
+
return
|
|
2093
|
+
}
|
|
2094
|
+
const signer = storageSigners[provider]
|
|
2095
|
+
if (!signer) {
|
|
2096
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(
|
|
2097
|
+
JSON.stringify({ error: `storage-redirect: unknown provider "${provider}" — supported: ${Object.keys(storageSigners).join(', ')}` }),
|
|
2098
|
+
)
|
|
2099
|
+
return
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
try {
|
|
2103
|
+
const signedUrl = await signer({ bucket, object, disposition: (disposition as 'inline' | 'attachment' | null) ?? undefined, filename: filename ?? undefined })
|
|
2104
|
+
res.writeHead(302, { location: signedUrl }).end()
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
res
|
|
2107
|
+
.writeHead(502, { 'content-type': 'application/json' })
|
|
2108
|
+
.end(JSON.stringify({ error: `storage-redirect: could not sign a URL for ${provider}://${bucket}/${object}: ${err instanceof Error ? err.message : String(err)}` }))
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2041
2112
|
const server = createServer(async (req, res) => {
|
|
2042
2113
|
if (!isAuthorized(req)) {
|
|
2043
2114
|
res
|
|
@@ -2484,6 +2555,11 @@ const server = createServer(async (req, res) => {
|
|
|
2484
2555
|
return
|
|
2485
2556
|
}
|
|
2486
2557
|
|
|
2558
|
+
if (req.method === 'GET' && pathname === '/storage-redirect') {
|
|
2559
|
+
await handleStorageRedirect(req, res)
|
|
2560
|
+
return
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2487
2563
|
const match = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/messages$/)
|
|
2488
2564
|
if (!match) {
|
|
2489
2565
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'not found' }))
|