dsh-codex-community 0.0.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/CONTRIBUTING.en.md +54 -0
  3. package/CONTRIBUTING.md +54 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +12 -0
  6. package/README.en.md +98 -0
  7. package/README.md +98 -0
  8. package/SECURITY.md +41 -0
  9. package/SUPPORT.md +17 -0
  10. package/THIRD_PARTY_NOTICES.md +25 -0
  11. package/codex-community.patch.yml +13 -0
  12. package/dist/client/index.js +1037 -0
  13. package/dist/host/index.mjs +98 -0
  14. package/dist/internal/authorization-bridge.mjs +662 -0
  15. package/dist/internal/authorization-commit-tracker.mjs +49 -0
  16. package/dist/internal/codex-authorization.mjs +202 -0
  17. package/dist/internal/codex-credential-store.mjs +164 -0
  18. package/dist/internal/codex-identifiers.mjs +4 -0
  19. package/dist/internal/codex-pi-provider.mjs +137 -0
  20. package/dist/internal/codex-provider-runtime.mjs +256 -0
  21. package/dist/internal/codex-route-adapter.mjs +133 -0
  22. package/dist/internal/codex-session-resources.mjs +64 -0
  23. package/dist/internal/failure-normalizer.mjs +456 -0
  24. package/dist/internal/image-policy.mjs +45 -0
  25. package/dist/internal/quota-observer.mjs +142 -0
  26. package/dist/internal/reliability.mjs +12 -0
  27. package/dist/internal/remote-image-input.mjs +801 -0
  28. package/dist/internal/session-preference-command.mjs +52 -0
  29. package/dist/internal/session-preferences.mjs +93 -0
  30. package/dist/internal/stream-resilience.mjs +273 -0
  31. package/docs/README.en.md +15 -0
  32. package/docs/README.md +15 -0
  33. package/docs/architecture.en.md +106 -0
  34. package/docs/architecture.md +106 -0
  35. package/docs/compatibility.en.md +53 -0
  36. package/docs/compatibility.md +53 -0
  37. package/docs/configuration.en.md +61 -0
  38. package/docs/configuration.md +61 -0
  39. package/docs/contribution-sources.en.md +35 -0
  40. package/docs/contribution-sources.md +35 -0
  41. package/docs/github-about.md +15 -0
  42. package/docs/releases/v0.0.1.acceptance.json +160 -0
  43. package/docs/releases/v0.0.1.md +174 -0
  44. package/docs/releasing.en.md +290 -0
  45. package/docs/releasing.md +290 -0
  46. package/docs/testing.en.md +85 -0
  47. package/docs/testing.md +85 -0
  48. package/docs/troubleshooting.en.md +47 -0
  49. package/docs/troubleshooting.md +47 -0
  50. package/package.json +144 -0
  51. package/types/client.d.ts +160 -0
  52. package/types/index.d.ts +22 -0
  53. package/types/reliability.d.ts +78 -0
@@ -0,0 +1,801 @@
1
+ import { lookup as dnsLookup } from "node:dns/promises"
2
+ import { request as httpRequest } from "node:http"
3
+ import { request as httpsRequest } from "node:https"
4
+ import { isIP } from "node:net"
5
+ import { promisify } from "node:util"
6
+ import {
7
+ brotliDecompress,
8
+ gunzip,
9
+ inflate,
10
+ } from "node:zlib"
11
+
12
+ import { CODEX_ROUTE_ID } from "./codex-identifiers.mjs"
13
+
14
+ const gunzipAsync = promisify(gunzip)
15
+ const inflateAsync = promisify(inflate)
16
+ const brotliDecompressAsync = promisify(brotliDecompress)
17
+
18
+ export const REMOTE_IMAGE_MEDIA_TYPES = Object.freeze([
19
+ "image/png",
20
+ "image/jpeg",
21
+ "image/webp",
22
+ "image/gif",
23
+ ])
24
+
25
+ export const DEFAULT_REMOTE_IMAGE_POLICY = Object.freeze({
26
+ maxBytes: 20 * 1024 * 1024,
27
+ maxRedirects: 3,
28
+ timeoutMs: 15_000,
29
+ mediaTypes: REMOTE_IMAGE_MEDIA_TYPES,
30
+ })
31
+
32
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])
33
+ const MAX_LOCATION_LENGTH = 8_192
34
+ const MAX_URL_LENGTH = 8_192
35
+ const MAX_RESPONSE_CHUNKS = 16_384
36
+ const MAX_CONCURRENT_REMOTE_IMAGES = 2
37
+ const MAX_QUEUED_REMOTE_IMAGES = 32
38
+
39
+ export class RemoteImageInputError extends Error {
40
+ constructor(message, code, options) {
41
+ super(message, options)
42
+ this.name = "RemoteImageInputError"
43
+ this.code = code
44
+ }
45
+ }
46
+
47
+ function positiveSafeInteger(value, name) {
48
+ if (!Number.isSafeInteger(value) || value <= 0) {
49
+ throw new TypeError(`${name} must be a positive safe integer`)
50
+ }
51
+ return value
52
+ }
53
+
54
+ function nonNegativeSafeInteger(value, name) {
55
+ if (!Number.isSafeInteger(value) || value < 0) {
56
+ throw new TypeError(`${name} must be a non-negative safe integer`)
57
+ }
58
+ return value
59
+ }
60
+
61
+ function resolveMediaTypes(value) {
62
+ if (!Array.isArray(value) || value.length === 0) {
63
+ throw new TypeError("mediaTypes must be a non-empty array")
64
+ }
65
+ const unique = []
66
+ for (const mediaType of value) {
67
+ if (!REMOTE_IMAGE_MEDIA_TYPES.includes(mediaType)) {
68
+ throw new TypeError(`unsupported remote image media type: ${String(mediaType)}`)
69
+ }
70
+ if (!unique.includes(mediaType)) unique.push(mediaType)
71
+ }
72
+ return Object.freeze(unique)
73
+ }
74
+
75
+ export function resolveRemoteImagePolicy(input = {}) {
76
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
77
+ throw new TypeError("remote image policy must be an object")
78
+ }
79
+ return Object.freeze({
80
+ maxBytes: positiveSafeInteger(input.maxBytes ?? DEFAULT_REMOTE_IMAGE_POLICY.maxBytes, "maxBytes"),
81
+ maxRedirects: nonNegativeSafeInteger(
82
+ input.maxRedirects ?? DEFAULT_REMOTE_IMAGE_POLICY.maxRedirects,
83
+ "maxRedirects",
84
+ ),
85
+ timeoutMs: positiveSafeInteger(input.timeoutMs ?? DEFAULT_REMOTE_IMAGE_POLICY.timeoutMs, "timeoutMs"),
86
+ mediaTypes: resolveMediaTypes(input.mediaTypes ?? DEFAULT_REMOTE_IMAGE_POLICY.mediaTypes),
87
+ })
88
+ }
89
+
90
+ function ipv4Value(address) {
91
+ const parts = address.split(".")
92
+ if (parts.length !== 4) return undefined
93
+ let value = 0n
94
+ for (const part of parts) {
95
+ if (!/^\d{1,3}$/u.test(part)) return undefined
96
+ const octet = Number(part)
97
+ if (octet > 255) return undefined
98
+ value = (value << 8n) | BigInt(octet)
99
+ }
100
+ return value
101
+ }
102
+
103
+ function ipv6Value(rawAddress) {
104
+ let address = rawAddress.toLowerCase()
105
+ if (address.startsWith("[") && address.endsWith("]")) address = address.slice(1, -1)
106
+ if (address.includes("%")) return undefined
107
+
108
+ const embeddedV4 = address.includes(".")
109
+ if (embeddedV4) {
110
+ const separator = address.lastIndexOf(":")
111
+ if (separator < 0) return undefined
112
+ const v4 = ipv4Value(address.slice(separator + 1))
113
+ if (v4 === undefined) return undefined
114
+ const high = ((v4 >> 16n) & 0xffffn).toString(16)
115
+ const low = (v4 & 0xffffn).toString(16)
116
+ address = `${address.slice(0, separator)}:${high}:${low}`
117
+ }
118
+
119
+ const halves = address.split("::")
120
+ if (halves.length > 2) return undefined
121
+ const left = halves[0] === "" ? [] : halves[0].split(":")
122
+ const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":")
123
+ if (left.some((part) => !/^[\da-f]{1,4}$/u.test(part))) return undefined
124
+ if (right.some((part) => !/^[\da-f]{1,4}$/u.test(part))) return undefined
125
+
126
+ const omitted = 8 - left.length - right.length
127
+ if ((halves.length === 1 && omitted !== 0) || (halves.length === 2 && omitted < 1)) return undefined
128
+ const parts = [...left, ...Array.from({ length: omitted }, () => "0"), ...right]
129
+ if (parts.length !== 8) return undefined
130
+ return parts.reduce((value, part) => (value << 16n) | BigInt(`0x${part}`), 0n)
131
+ }
132
+
133
+ function inCidr(value, network, prefix, bits) {
134
+ const shift = BigInt(bits - prefix)
135
+ return (value >> shift) === (network >> shift)
136
+ }
137
+
138
+ const IPV4_BLOCKS = [
139
+ ["0.0.0.0", 8],
140
+ ["10.0.0.0", 8],
141
+ ["100.64.0.0", 10],
142
+ ["127.0.0.0", 8],
143
+ ["168.63.129.16", 32],
144
+ ["169.254.0.0", 16],
145
+ ["172.16.0.0", 12],
146
+ ["192.0.0.0", 24],
147
+ ["192.0.2.0", 24],
148
+ ["192.88.99.0", 24],
149
+ ["192.168.0.0", 16],
150
+ ["198.18.0.0", 15],
151
+ ["198.51.100.0", 24],
152
+ ["203.0.113.0", 24],
153
+ ["224.0.0.0", 4],
154
+ ["240.0.0.0", 4],
155
+ ].map(([address, prefix]) => [ipv4Value(address), prefix])
156
+
157
+ const IPV6_GLOBAL = [ipv6Value("2000::"), 3]
158
+ const IPV6_BLOCKS = [
159
+ ["2001::", 23],
160
+ ["2001:db8::", 32],
161
+ ["2002::", 16],
162
+ ["3fff::", 20],
163
+ ].map(([address, prefix]) => [ipv6Value(address), prefix])
164
+
165
+ /** Return true only for an ordinary globally routable unicast address. */
166
+ export function isPublicIpAddress(address) {
167
+ const family = isIP(address)
168
+ if (family === 4) {
169
+ const value = ipv4Value(address)
170
+ return value !== undefined
171
+ && !IPV4_BLOCKS.some(([network, prefix]) => inCidr(value, network, prefix, 32))
172
+ }
173
+ if (family === 6) {
174
+ const value = ipv6Value(address)
175
+ if (value === undefined) return false
176
+ if (!inCidr(value, IPV6_GLOBAL[0], IPV6_GLOBAL[1], 128)) return false
177
+ return !IPV6_BLOCKS.some(([network, prefix]) => inCidr(value, network, prefix, 128))
178
+ }
179
+ return false
180
+ }
181
+
182
+ function unbracket(hostname) {
183
+ return hostname.startsWith("[") && hostname.endsWith("]")
184
+ ? hostname.slice(1, -1)
185
+ : hostname
186
+ }
187
+
188
+ function normalizedHostname(hostname) {
189
+ const host = unbracket(hostname).toLowerCase()
190
+ return host.endsWith(".") ? host.slice(0, -1) : host
191
+ }
192
+
193
+ const FORBIDDEN_HOST_SUFFIXES = [
194
+ ".home.arpa",
195
+ ".internal",
196
+ ".invalid",
197
+ ".local",
198
+ ".localhost",
199
+ ".test",
200
+ ]
201
+
202
+ function hasForbiddenHostSuffix(hostname) {
203
+ return FORBIDDEN_HOST_SUFFIXES.some((suffix) => (
204
+ hostname === suffix.slice(1) || hostname.endsWith(suffix)
205
+ ))
206
+ }
207
+
208
+ function assertRemoteUrl(input, base) {
209
+ let url
210
+ try {
211
+ url = base === undefined ? new URL(input) : new URL(input, base)
212
+ } catch (error) {
213
+ throw new RemoteImageInputError("remote image URL is invalid", "INVALID_URL", { cause: error })
214
+ }
215
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
216
+ throw new RemoteImageInputError("remote image URL must use http or https", "UNSUPPORTED_PROTOCOL")
217
+ }
218
+ if (url.username !== "" || url.password !== "") {
219
+ throw new RemoteImageInputError("remote image URL must not contain user information", "URL_CREDENTIALS")
220
+ }
221
+ if (url.hostname === "") {
222
+ throw new RemoteImageInputError("remote image URL must contain a host", "INVALID_URL")
223
+ }
224
+ if (url.href.length > MAX_URL_LENGTH) {
225
+ throw new RemoteImageInputError("remote image URL is too long", "INVALID_URL")
226
+ }
227
+
228
+ const hostname = normalizedHostname(url.hostname)
229
+ if (hostname === "localhost" || hasForbiddenHostSuffix(hostname)) {
230
+ throw new RemoteImageInputError("remote image URL targets a non-public host", "UNSAFE_ADDRESS")
231
+ }
232
+ if (isIP(hostname) === 0 && !hostname.includes(".")) {
233
+ throw new RemoteImageInputError("remote image URL host must be fully qualified", "UNSAFE_ADDRESS")
234
+ }
235
+ return url
236
+ }
237
+
238
+ function normalizeLookupResult(result) {
239
+ const entries = Array.isArray(result) ? result : [result]
240
+ const addresses = []
241
+ for (const entry of entries) {
242
+ const address = typeof entry === "string" ? entry : entry?.address
243
+ const family = typeof entry === "object" && entry !== null ? entry.family : isIP(address)
244
+ const detected = typeof address === "string" ? isIP(address) : 0
245
+ if (detected === 0 || (family !== 4 && family !== 6) || detected !== Number(family)) {
246
+ throw new RemoteImageInputError("DNS returned an invalid address", "DNS_FAILED")
247
+ }
248
+ if (!isPublicIpAddress(address)) {
249
+ throw new RemoteImageInputError("remote image URL resolved to a non-public address", "UNSAFE_ADDRESS")
250
+ }
251
+ const key = `${detected}:${address}`
252
+ if (!addresses.some((candidate) => candidate.key === key)) {
253
+ addresses.push({ key, address, family: detected })
254
+ }
255
+ }
256
+ if (addresses.length === 0) {
257
+ throw new RemoteImageInputError("remote image host did not resolve", "DNS_FAILED")
258
+ }
259
+ return addresses.map(({ address, family }) => Object.freeze({ address, family }))
260
+ }
261
+
262
+ function awaitWithSignal(value, signal) {
263
+ if (signal.aborted) return Promise.reject(abortReason(signal))
264
+ return new Promise((resolve, reject) => {
265
+ const onAbort = () => {
266
+ signal.removeEventListener("abort", onAbort)
267
+ reject(abortReason(signal))
268
+ }
269
+ signal.addEventListener("abort", onAbort, { once: true })
270
+ Promise.resolve(value).then(resolve, reject).finally(() => {
271
+ signal.removeEventListener("abort", onAbort)
272
+ })
273
+ })
274
+ }
275
+
276
+ async function resolveAddresses(url, lookup, signal) {
277
+ const hostname = normalizedHostname(url.hostname)
278
+ if (isIP(hostname) !== 0) return normalizeLookupResult([{ address: hostname, family: isIP(hostname) }])
279
+ let result
280
+ try {
281
+ result = await awaitWithSignal(
282
+ Promise.resolve().then(() => lookup(hostname, { all: true, verbatim: true })),
283
+ signal,
284
+ )
285
+ } catch (error) {
286
+ if (error instanceof RemoteImageInputError) throw error
287
+ throw new RemoteImageInputError("remote image host lookup failed", "DNS_FAILED", { cause: error })
288
+ }
289
+ return normalizeLookupResult(result)
290
+ }
291
+
292
+ function pinnedLookup(hostname, addresses) {
293
+ const expected = normalizedHostname(hostname)
294
+ return (requestedHostname, options, callback) => {
295
+ const done = typeof options === "function" ? options : callback
296
+ const lookupOptions = typeof options === "object" && options !== null ? options : {}
297
+ if (typeof done !== "function") throw new TypeError("lookup callback is required")
298
+ if (normalizedHostname(requestedHostname) !== expected) {
299
+ done(new RemoteImageInputError("request attempted an unpinned DNS lookup", "DNS_REBINDING_GUARD"))
300
+ return
301
+ }
302
+ const requestedFamily = Number(lookupOptions.family) || 0
303
+ const candidates = requestedFamily === 0
304
+ ? addresses
305
+ : addresses.filter((candidate) => candidate.family === requestedFamily)
306
+ if (candidates.length === 0) {
307
+ done(new RemoteImageInputError("no pinned address matches the requested family", "DNS_REBINDING_GUARD"))
308
+ return
309
+ }
310
+ if (lookupOptions.all === true) {
311
+ done(null, candidates.map(({ address, family }) => ({ address, family })))
312
+ return
313
+ }
314
+ done(null, candidates[0].address, candidates[0].family)
315
+ }
316
+ }
317
+
318
+ function openNodeResponse(url, options) {
319
+ const request = url.protocol === "https:" ? httpsRequest : httpRequest
320
+ return new Promise((resolve, reject) => {
321
+ const outgoing = request(url, {
322
+ agent: false,
323
+ headers: options.headers,
324
+ lookup: options.lookup,
325
+ maxHeaderSize: 16 * 1024,
326
+ method: "GET",
327
+ signal: options.signal,
328
+ }, resolve)
329
+ outgoing.once("error", reject)
330
+ outgoing.end()
331
+ })
332
+ }
333
+
334
+ function headerValue(headers, name) {
335
+ const value = headers?.[name]
336
+ if (Array.isArray(value)) return value.join(",")
337
+ return typeof value === "string" ? value : undefined
338
+ }
339
+
340
+ function contentLength(response) {
341
+ const header = headerValue(response.headers, "content-length")
342
+ if (header === undefined) return undefined
343
+ if (!/^\d+$/u.test(header)) {
344
+ throw new RemoteImageInputError("remote image response has an invalid content length", "INVALID_RESPONSE")
345
+ }
346
+ const value = Number(header)
347
+ if (!Number.isSafeInteger(value)) {
348
+ throw new RemoteImageInputError("remote image response has an invalid content length", "INVALID_RESPONSE")
349
+ }
350
+ return value
351
+ }
352
+
353
+ async function readBounded(response, maxBytes, signal) {
354
+ const declared = contentLength(response)
355
+ if (declared !== undefined && declared > maxBytes) {
356
+ response.destroy?.()
357
+ throw new RemoteImageInputError("remote image response exceeds the byte limit", "RESPONSE_TOO_LARGE")
358
+ }
359
+ const chunks = []
360
+ let bytes = 0
361
+ for await (const value of response) {
362
+ if (signal.aborted) throw signal.reason
363
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value)
364
+ bytes += chunk.byteLength
365
+ if (bytes > maxBytes) {
366
+ response.destroy?.()
367
+ throw new RemoteImageInputError("remote image response exceeds the byte limit", "RESPONSE_TOO_LARGE")
368
+ }
369
+ if (chunks.length >= MAX_RESPONSE_CHUNKS) {
370
+ response.destroy?.()
371
+ throw new RemoteImageInputError("remote image response is excessively fragmented", "INVALID_RESPONSE")
372
+ }
373
+ chunks.push(chunk)
374
+ }
375
+ return Buffer.concat(chunks, bytes)
376
+ }
377
+
378
+ async function decodeBody(data, response, maxBytes) {
379
+ const raw = headerValue(response.headers, "content-encoding")?.trim().toLowerCase()
380
+ const encoding = raw === undefined || raw === "" ? "identity" : raw
381
+ if (encoding.includes(",")) {
382
+ throw new RemoteImageInputError("stacked content encodings are not accepted", "UNSUPPORTED_CONTENT_ENCODING")
383
+ }
384
+ if (encoding === "identity") return data
385
+
386
+ let decode
387
+ if (encoding === "gzip" || encoding === "x-gzip") decode = gunzipAsync
388
+ else if (encoding === "deflate") decode = inflateAsync
389
+ else if (encoding === "br") decode = brotliDecompressAsync
390
+ else {
391
+ throw new RemoteImageInputError("remote image content encoding is not supported", "UNSUPPORTED_CONTENT_ENCODING")
392
+ }
393
+
394
+ try {
395
+ const decoded = await decode(data, { maxOutputLength: maxBytes })
396
+ if (decoded.byteLength > maxBytes) {
397
+ throw new RemoteImageInputError("decompressed remote image exceeds the byte limit", "RESPONSE_TOO_LARGE")
398
+ }
399
+ return decoded
400
+ } catch (error) {
401
+ if (error instanceof RemoteImageInputError) throw error
402
+ if (error?.code === "ERR_BUFFER_TOO_LARGE" || /maxOutputLength|larger than/u.test(error?.message ?? "")) {
403
+ throw new RemoteImageInputError("decompressed remote image exceeds the byte limit", "RESPONSE_TOO_LARGE", {
404
+ cause: error,
405
+ })
406
+ }
407
+ throw new RemoteImageInputError("remote image content encoding is invalid", "INVALID_CONTENT_ENCODING", {
408
+ cause: error,
409
+ })
410
+ }
411
+ }
412
+
413
+ function responseMediaType(response, allowed) {
414
+ const raw = headerValue(response.headers, "content-type")
415
+ const mediaType = raw?.split(";", 1)[0].trim().toLowerCase()
416
+ if (mediaType === undefined || !allowed.includes(mediaType)) {
417
+ throw new RemoteImageInputError("remote response is not an allowed image media type", "UNSUPPORTED_MEDIA_TYPE")
418
+ }
419
+ return mediaType
420
+ }
421
+
422
+ function safeImageName(url, mediaType) {
423
+ const lastSegment = url.pathname.split("/").at(-1)
424
+ let decoded = lastSegment
425
+ try {
426
+ decoded = decodeURIComponent(lastSegment)
427
+ } catch {
428
+ // Keep the URL-encoded segment; it is display-only and never interpreted as a path.
429
+ }
430
+ const cleaned = decoded
431
+ ?.replace(/[\u0000-\u001f\u007f/\\]/gu, "_")
432
+ .trim()
433
+ .slice(0, 120)
434
+ if (cleaned) return cleaned
435
+ const extension = mediaType === "image/jpeg" ? "jpg" : mediaType.slice("image/".length)
436
+ return `remote-image.${extension}`
437
+ }
438
+
439
+ function deadline(parentSignal, timeoutMs) {
440
+ const controller = new AbortController()
441
+ let timer
442
+ const abortFromParent = () => {
443
+ controller.abort(parentSignal.reason ?? new RemoteImageInputError("remote image request was aborted", "ABORTED"))
444
+ }
445
+ if (parentSignal?.aborted) abortFromParent()
446
+ else parentSignal?.addEventListener("abort", abortFromParent, { once: true })
447
+ if (!controller.signal.aborted) {
448
+ timer = setTimeout(() => {
449
+ controller.abort(new RemoteImageInputError("remote image request timed out", "TIMEOUT"))
450
+ }, timeoutMs)
451
+ }
452
+ return {
453
+ signal: controller.signal,
454
+ dispose() {
455
+ if (timer !== undefined) clearTimeout(timer)
456
+ parentSignal?.removeEventListener("abort", abortFromParent)
457
+ },
458
+ }
459
+ }
460
+
461
+ function abortReason(signal) {
462
+ if (signal.reason instanceof Error) return signal.reason
463
+ return new RemoteImageInputError("remote image request was aborted", "ABORTED")
464
+ }
465
+
466
+ function linkedAbortSignal(...sources) {
467
+ const controller = new AbortController()
468
+ const listeners = []
469
+ const abortFrom = (source) => {
470
+ if (controller.signal.aborted) return
471
+ controller.abort(source.reason ?? new RemoteImageInputError("remote image request was aborted", "ABORTED"))
472
+ }
473
+
474
+ for (const source of sources) {
475
+ if (source === undefined) continue
476
+ if (source.aborted) {
477
+ abortFrom(source)
478
+ break
479
+ }
480
+ const listener = () => abortFrom(source)
481
+ source.addEventListener("abort", listener, { once: true })
482
+ listeners.push({ source, listener })
483
+ }
484
+
485
+ return {
486
+ signal: controller.signal,
487
+ dispose() {
488
+ for (const { source, listener } of listeners) {
489
+ source.removeEventListener("abort", listener)
490
+ }
491
+ },
492
+ }
493
+ }
494
+
495
+ class RemoteImageWorkLimiter {
496
+ #active = 0
497
+ #queue = []
498
+ #disposedReason
499
+ #idleWaiters = []
500
+
501
+ run(signal, task) {
502
+ if (this.#disposedReason !== undefined) return Promise.reject(this.#disposedReason)
503
+ if (signal?.aborted === true) return Promise.reject(abortReason(signal))
504
+ if (this.#active >= MAX_CONCURRENT_REMOTE_IMAGES && this.#queue.length >= MAX_QUEUED_REMOTE_IMAGES) {
505
+ return Promise.reject(new RemoteImageInputError(
506
+ "too many remote image requests are pending",
507
+ "TOO_MANY_REQUESTS",
508
+ ))
509
+ }
510
+
511
+ return new Promise((resolve, reject) => {
512
+ const entry = {
513
+ signal,
514
+ task,
515
+ resolve,
516
+ reject,
517
+ started: false,
518
+ onAbort: undefined,
519
+ }
520
+ entry.onAbort = () => {
521
+ if (entry.started) return
522
+ const index = this.#queue.indexOf(entry)
523
+ if (index < 0) return
524
+ this.#queue.splice(index, 1)
525
+ signal.removeEventListener("abort", entry.onAbort)
526
+ reject(abortReason(signal))
527
+ }
528
+ signal?.addEventListener("abort", entry.onAbort, { once: true })
529
+ this.#queue.push(entry)
530
+ this.#drain()
531
+ })
532
+ }
533
+
534
+ dispose(reason = new RemoteImageInputError("remote image middleware was disposed", "ABORTED")) {
535
+ if (this.#disposedReason === undefined) {
536
+ this.#disposedReason = reason
537
+ for (const entry of this.#queue.splice(0)) {
538
+ entry.signal?.removeEventListener("abort", entry.onAbort)
539
+ entry.reject(reason)
540
+ }
541
+ }
542
+ if (this.#active === 0) return Promise.resolve()
543
+ return new Promise((resolve) => { this.#idleWaiters.push(resolve) })
544
+ }
545
+
546
+ #drain() {
547
+ if (this.#disposedReason !== undefined) return
548
+ while (this.#active < MAX_CONCURRENT_REMOTE_IMAGES && this.#queue.length > 0) {
549
+ const entry = this.#queue.shift()
550
+ entry.started = true
551
+ entry.signal?.removeEventListener("abort", entry.onAbort)
552
+ if (entry.signal?.aborted === true) {
553
+ entry.reject(abortReason(entry.signal))
554
+ continue
555
+ }
556
+
557
+ this.#active += 1
558
+ void Promise.resolve()
559
+ .then(() => {
560
+ if (entry.signal?.aborted === true) throw abortReason(entry.signal)
561
+ return entry.task()
562
+ })
563
+ .then(entry.resolve, entry.reject)
564
+ .finally(() => {
565
+ this.#active -= 1
566
+ if (this.#disposedReason !== undefined && this.#active === 0) {
567
+ for (const resolve of this.#idleWaiters.splice(0)) resolve()
568
+ } else {
569
+ this.#drain()
570
+ }
571
+ })
572
+ }
573
+ }
574
+ }
575
+
576
+ function discard(response) {
577
+ response.resume?.()
578
+ response.destroy?.()
579
+ }
580
+
581
+ /**
582
+ * Download one remote image through a DNS-pinned, redirect-aware request path.
583
+ * Network dependencies are injectable so the security boundary can be tested
584
+ * without contacting external hosts.
585
+ */
586
+ export async function downloadRemoteImage(input, options = {}) {
587
+ const policy = resolveRemoteImagePolicy(options.policy)
588
+ const lookup = options.lookup ?? dnsLookup
589
+ const openResponse = options.openResponse ?? openNodeResponse
590
+ let current = assertRemoteUrl(input)
591
+ const active = deadline(options.signal, policy.timeoutMs)
592
+ let redirects = 0
593
+
594
+ try {
595
+ while (true) {
596
+ if (active.signal.aborted) throw abortReason(active.signal)
597
+ const addresses = await resolveAddresses(current, lookup, active.signal)
598
+ if (active.signal.aborted) throw abortReason(active.signal)
599
+
600
+ let response
601
+ try {
602
+ response = await awaitWithSignal(
603
+ Promise.resolve().then(() => openResponse(current, {
604
+ headers: {
605
+ accept: policy.mediaTypes.join(", "),
606
+ "accept-encoding": "gzip, deflate, br",
607
+ "user-agent": "dsh-codex remote-image",
608
+ },
609
+ lookup: pinnedLookup(current.hostname, addresses),
610
+ signal: active.signal,
611
+ })),
612
+ active.signal,
613
+ )
614
+ } catch (error) {
615
+ if (active.signal.aborted) throw abortReason(active.signal)
616
+ if (error instanceof RemoteImageInputError) throw error
617
+ throw new RemoteImageInputError("remote image request failed", "NETWORK", { cause: error })
618
+ }
619
+
620
+ const status = response.statusCode
621
+ if (REDIRECT_STATUSES.has(status)) {
622
+ const location = headerValue(response.headers, "location")
623
+ discard(response)
624
+ if (location === undefined || location.length === 0 || location.length > MAX_LOCATION_LENGTH) {
625
+ throw new RemoteImageInputError("remote image redirect location is missing or invalid", "INVALID_REDIRECT")
626
+ }
627
+ if (redirects >= policy.maxRedirects) {
628
+ throw new RemoteImageInputError("remote image exceeded the redirect limit", "TOO_MANY_REDIRECTS")
629
+ }
630
+ current = assertRemoteUrl(location, current)
631
+ redirects += 1
632
+ continue
633
+ }
634
+ if (status !== 200) {
635
+ discard(response)
636
+ throw new RemoteImageInputError(`remote image request returned HTTP ${String(status)}`, "HTTP_STATUS")
637
+ }
638
+
639
+ try {
640
+ const mediaType = responseMediaType(response, policy.mediaTypes)
641
+ const encoded = await awaitWithSignal(
642
+ readBounded(response, policy.maxBytes, active.signal),
643
+ active.signal,
644
+ )
645
+ const data = await awaitWithSignal(
646
+ decodeBody(encoded, response, policy.maxBytes),
647
+ active.signal,
648
+ )
649
+ if (data.byteLength === 0) {
650
+ throw new RemoteImageInputError("remote image response is empty", "INVALID_RESPONSE")
651
+ }
652
+ return Object.freeze({
653
+ data: Uint8Array.from(data),
654
+ mediaType,
655
+ name: safeImageName(current, mediaType),
656
+ })
657
+ } catch (error) {
658
+ response.destroy?.()
659
+ if (active.signal.aborted) throw abortReason(active.signal)
660
+ if (error instanceof RemoteImageInputError) throw error
661
+ throw new RemoteImageInputError("remote image response failed", "NETWORK", { cause: error })
662
+ }
663
+ }
664
+ } finally {
665
+ active.dispose()
666
+ }
667
+ }
668
+
669
+ function attachmentPolicy(attachments, policy) {
670
+ if (attachments === null || typeof attachments !== "object" || typeof attachments.saveImage !== "function") {
671
+ throw new RemoteImageInputError("durable attachment service is unavailable", "ATTACHMENT_UNAVAILABLE")
672
+ }
673
+ const limits = attachments.imageLimits
674
+ if (limits === null || typeof limits !== "object") {
675
+ throw new RemoteImageInputError("durable attachment limits are unavailable", "ATTACHMENT_UNAVAILABLE")
676
+ }
677
+ const maxBytes = Math.min(
678
+ policy.maxBytes,
679
+ positiveSafeInteger(limits.maxImageBytes, "attachments.imageLimits.maxImageBytes"),
680
+ positiveSafeInteger(limits.maxMessageImageBytes, "attachments.imageLimits.maxMessageImageBytes"),
681
+ )
682
+ const deployed = Array.isArray(limits.mediaTypes) ? limits.mediaTypes : []
683
+ const mediaTypes = policy.mediaTypes.filter((mediaType) => deployed.includes(mediaType))
684
+ if (mediaTypes.length === 0) {
685
+ throw new RemoteImageInputError("deployment accepts no supported remote image media types", "UNSUPPORTED_MEDIA_TYPE")
686
+ }
687
+ return { ...policy, maxBytes, mediaTypes }
688
+ }
689
+
690
+ /** Download, validate, and commit one URL image before exposing its model block. */
691
+ export async function saveRemoteImage(attachments, input, options = {}) {
692
+ const policy = attachmentPolicy(attachments, resolveRemoteImagePolicy(options.policy))
693
+ const downloaded = await downloadRemoteImage(input, { ...options, policy })
694
+ if (options.signal?.aborted) throw abortReason(options.signal)
695
+ const attachment = await attachments.saveImage(downloaded)
696
+ if (options.signal?.aborted) throw abortReason(options.signal)
697
+ return Object.freeze({
698
+ attachment,
699
+ block: Object.freeze({ type: "image", attachment }),
700
+ })
701
+ }
702
+
703
+ function isHttpUrlString(value) {
704
+ return typeof value === "string" && /^https?:\/\//iu.test(value.trim())
705
+ }
706
+
707
+ function displayRemoteUrl(input) {
708
+ const url = assertRemoteUrl(input)
709
+ url.search = ""
710
+ url.hash = ""
711
+ return url.toString()
712
+ }
713
+
714
+ function imageValue(attachment) {
715
+ return {
716
+ attachmentId: attachment.attachmentId,
717
+ mediaType: attachment.mediaType,
718
+ bytes: attachment.bytes,
719
+ width: attachment.width,
720
+ height: attachment.height,
721
+ ...(attachment.name === undefined ? {} : { name: attachment.name }),
722
+ ...(attachment.originalDimensions === undefined
723
+ ? {}
724
+ : { originalDimensions: { ...attachment.originalDimensions } }),
725
+ }
726
+ }
727
+
728
+ function activeModelRoute(exec) {
729
+ const routed = exec.agent?.session.requestHeader()?.config
730
+ return {
731
+ provider: routed?.provider ?? exec.agent?.options.provider,
732
+ model: routed?.model ?? exec.agent?.options.model,
733
+ }
734
+ }
735
+
736
+ async function assertImageCapableRoute(ctx, exec, requestedUrl, signal = exec.signal) {
737
+ const { provider, model } = activeModelRoute(exec)
738
+ const llm = ctx.get("llm")
739
+ if (provider === undefined || model === undefined || llm === undefined) {
740
+ throw new Error(`cannot read "${displayRemoteUrl(requestedUrl)}" as an image: the current model route could not be resolved`)
741
+ }
742
+ const active = await awaitWithSignal(
743
+ Promise.resolve().then(() => llm.resolveModelInfo(provider, model, signal)),
744
+ signal,
745
+ )
746
+ if (active.inputModalities === undefined || !active.inputModalities.includes("image")) {
747
+ throw new Error(`cannot read "${displayRemoteUrl(requestedUrl)}" as an image: model "${model}" does not declare image input; switch to an image-capable model to read images`)
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Build a public `tools/execute` middleware that handles only HTTP(S) values
753
+ * passed to the existing `read_image` tool. The tool registry validates this
754
+ * value against the original output schema and invokes its original renderer;
755
+ * ordinary filesystem paths always delegate to `next()` unchanged.
756
+ */
757
+ export function createReadImageUrlMiddleware(ctx, options = {}) {
758
+ if (ctx === null || typeof ctx !== "object" || typeof ctx.get !== "function") {
759
+ throw new TypeError("a Cordis context with public service lookup is required")
760
+ }
761
+ const limiter = new RemoteImageWorkLimiter()
762
+ const lifetime = new AbortController()
763
+ let disposal
764
+ const middleware = async (exec, next) => {
765
+ const requested = exec.name === "read_image" ? exec.arguments?.file_path : undefined
766
+ if (!isHttpUrlString(requested)) return next()
767
+ if (activeModelRoute(exec).provider !== CODEX_ROUTE_ID) return next()
768
+
769
+ const normalized = requested.trim()
770
+ const active = linkedAbortSignal(exec.signal, lifetime.signal)
771
+ try {
772
+ if (active.signal.aborted) throw abortReason(active.signal)
773
+ return await limiter.run(active.signal, async () => {
774
+ await assertImageCapableRoute(ctx, exec, normalized, active.signal)
775
+ const attachments = ctx.get("attachments")
776
+ const { attachment } = await saveRemoteImage(attachments, normalized, {
777
+ ...options,
778
+ signal: active.signal,
779
+ })
780
+ return {
781
+ isError: false,
782
+ value: {
783
+ path: displayRemoteUrl(normalized),
784
+ image: imageValue(attachment),
785
+ },
786
+ }
787
+ })
788
+ } finally {
789
+ active.dispose()
790
+ }
791
+ }
792
+ middleware.dispose = () => {
793
+ if (disposal !== undefined) return disposal
794
+ const reason = new RemoteImageInputError("remote image middleware was disposed", "ABORTED")
795
+ const pending = limiter.dispose(reason)
796
+ disposal = Promise.resolve(pending)
797
+ lifetime.abort(reason)
798
+ return disposal
799
+ }
800
+ return middleware
801
+ }