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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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' }))