diff --git a/src/adapters/devin.ts b/src/adapters/devin.ts index 06fafe3eae..d1b0c7bc0e 100644 --- a/src/adapters/devin.ts +++ b/src/adapters/devin.ts @@ -209,6 +209,17 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string { return content.map((part) => (part.type === "text" ? part.text : "")).filter(Boolean).join("\n"); } +const MAX_DEVIN_REMOTE_IMAGE_URL_CHARS = 8_192; + +function boundedDevinRemoteImageReference(imageUrl: string): string | undefined { + if (imageUrl.length > MAX_DEVIN_REMOTE_IMAGE_URL_CHARS) return undefined; + try { + return new URL(imageUrl).protocol === "https:" ? imageUrl : undefined; + } catch { + return undefined; + } +} + /** * Convert inbound content parts to the multimodal shape the wire encoder accepts. * @@ -217,9 +228,10 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string { * text-only string and a message whose only content was an image was dropped * entirely, which is why a pasted screenshot killed the turn and the only * workaround was running OCR before sending. A data: URL carries everything - * field #10 needs; a remote https URL cannot be inlined without a fetch, so it - * stays as an explicit text reference rather than pretending the model can see - * a picture it cannot. Video has no Devin field and is skipped. + * field #10 needs; a bounded remote https URL cannot be inlined without a fetch, + * so it stays as an explicit text reference rather than pretending the model can + * see a picture it cannot. Unsupported and oversized references become a fixed + * omission marker, never attacker-sized prompt text. Video has no Devin field. */ function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): string | ContentPart[] { if (typeof content === "string" || !Array.isArray(content)) return content ?? ""; @@ -230,7 +242,13 @@ function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): st } else if (part.type === "image") { const m = part.imageUrl.match(/^data:([^;]+);base64,(.+)$/); if (m) out.push({ type: "image", mimeType: m[1]!, base64Data: m[2]! }); - else out.push({ type: "text", text: `[image url: ${part.imageUrl}]` }); + else { + const remoteReference = boundedDevinRemoteImageReference(part.imageUrl); + out.push({ + type: "text", + text: remoteReference ? `[image url: ${remoteReference}]` : "[image omitted: unsupported or oversized URL]", + }); + } } } return out; diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 1952e3615f..f510e2c305 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -141,3 +141,7 @@ medium/high/max UID before accepting a suffix already present in the model id. The merged `devin` provider uses this resolver for every account, whichever login path minted the credential. Omitted effort preserves an explicit variant; unrelated model families retain their existing suffix precedence. + +## Devin image boundary + +The registered Devin implementation in `src/adapters/devin.ts` maps data URLs to its native image field. Its textual fallback accepts only bounded HTTPS references and emits a fixed-size omission marker for unsupported or oversized values. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 8b27dd5688..012d9faf88 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -247,3 +247,7 @@ Translated Chat request construction uses the [inline-image budget](../transport The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +## Devin image references + +When compatible inbound content reaches `src/adapters/devin.ts`, inline data URLs use Devin's native image field. Only bounded HTTPS remote references become prompt text; all other remote values become a fixed-size omission marker. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 2362e46bcd..19ab6456e6 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -292,3 +292,7 @@ byte-limit boundaries. Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached. + +## Devin image compatibility + +For Responses content routed through `src/adapters/devin.ts`, inline data URLs use Devin's image field. Bounded HTTPS references remain visible as text, while unsupported or oversized references are represented by a fixed-size omission marker. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 55f017685b..ea7fa0c20a 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -106,3 +106,7 @@ Combo child requests normalize effort and thinking controls against the selected `src/adapters/cursor.ts` surfaces the first bare context overflow before attempting conversation remint on later eligible requests. `cursorClientThreadOwner` recognizes both client thread aliases; `src/adapters/cursor/thread-continuity.ts` limits recovery to three remints per retained identity-scoped owner, with a one-hour idle TTL and 2,048-entry bound. Conversation-only requests have no stable owner and do not automatically remint. Quota/rate errors, tool-result resumes, partial output, local side effects, isolated helper/shadow requests and compaction remain fail-closed. Isolated requests neither consume the parent allowance nor invalidate its checkpoint. Eligible overflow checks refresh existing retention timestamps and LRU position even after the cap is exhausted, without allocating absent scopes. Retention expiry, eviction or process restart resets the in-memory allowance; this is not a persistent lifetime cap or semantic-progress policy. Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached. + +## Cross-adapter image boundary + +Cursor transport behavior remains independent from the Devin mapping in `src/adapters/devin.ts`: Devin restricts text-rendered remote image references to bounded HTTPS URLs and uses a fixed-size marker otherwise. diff --git a/structure/runtime.md b/structure/runtime.md index a09d007d7b..27a9a42575 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -350,3 +350,7 @@ Exact [model input declarations](config.md#explicit-per-model-capability-declara Provider-scoped approval reviewer settings are projected by the [catalog owner](catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence. + +### Devin multimodal boundary + +`src/adapters/devin.ts` forwards data-URL images on the native image field. Remote references become prompt text only when they are bounded HTTPS URLs; unsupported or oversized values become a fixed-size omission marker. diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 830d1af7a9..0b9cd6182c 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -33,3 +33,7 @@ plus exact separators, without joining a second full JSON array. `src/lib/admiss truncates diagnostic text at UTF-8 code-point boundaries without allocating arrays per character; byte sizing retains TextEncoder's coercion behavior for legacy non-string runtime callers. These optimizations do not add request queues, retry policies, or RSS-based admission gates. + +## Devin remote-image references + +`src/adapters/devin.ts` bounds remote image URLs before converting them to text. Values outside that bound become a constant-size marker, so adapter serialization cannot turn a fixed image admission charge into attacker-sized prompt buffers. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 10b5be6a6b..714e582b27 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -142,3 +142,7 @@ The [explicit model-capability contract](../config.md#explicit-per-model-capabil Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Renamed fixed-key providers receive [missing reasoning metadata](../catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence. + +## Devin multimodal transport + +`src/adapters/devin.ts` maps inline data URLs to Devin's native image field. It preserves bounded HTTPS image references as text and replaces unsupported or oversized references with a fixed-size marker before protobuf construction. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 1f1bd3cd2f..5816d4b1cd 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -593,7 +593,7 @@ Pool quota producers and account commands follow the [bounded raw-observation co Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate. -Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached. +Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached. At the `src/adapters/` boundary, Devin renders only bounded HTTPS image references as text and replaces all other remote values with a fixed-size omission marker. The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/tests/providers/devin-image-passthrough.test.ts b/tests/providers/devin-image-passthrough.test.ts index e9445abe2f..b9a5ec57e5 100644 --- a/tests/providers/devin-image-passthrough.test.ts +++ b/tests/providers/devin-image-passthrough.test.ts @@ -53,6 +53,17 @@ describe("user image passthrough", () => { const user = items.find(i => i.role === "user")!; expect(user.content).toEqual([{ type: "text", text: "[image url: https://example.com/pic.png]" }]); }); + + test("an oversized remote image URL is not copied into prompt text", () => { + const attackerControlledSuffix = "a".repeat(9_000); + const items = mapOcxMessagesToDevin(parsedWith([{ + role: "user", + content: [{ type: "image", imageUrl: `https://example.com/${attackerControlledSuffix}` }], + }])); + const user = items.find(i => i.role === "user")!; + expect(user.content).toEqual([{ type: "text", text: "[image omitted: unsupported or oversized URL]" }]); + expect(JSON.stringify(user.content)).not.toContain(attackerControlledSuffix); + }); }); describe("tool-result image passthrough", () => {