authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-26 11:09:35+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-26 03:09:35-05:00
logdef34c28b8841002bb97a3238b82ac758cbdaa0f
tree09877d4bb294b67d3d5d915005cb2b00d0fea8a7
parent2679d279ce4b4879290bc756cf219b10f77e1873
signature Signed by PGP key B5690EEEBB952194

fix(server): let Antigravity inspect unsupported files by path (#13339)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

7 files changed, 182 insertions(+), 61 deletions(-)

apps/server/src/provider/Layers/AntigravityAdapter.ts+3-3
......@@ -786,9 +786,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
786786 stopOwned,
787787 Effect.gen(function* () {
788788 const mcp = McpProviderSession.readMcpProviderSession(input.threadId);
789 // The attachments dir grant lets the agent read pasted files at
790 // the paths ProviderService injects into the turn text. It is a
791 // leaf directory holding only uploads.
789 // The attachments dir grant lets the agent read path-only uploads
790 // at the paths ProviderService injects into the turn text. It is
791 // a leaf directory holding only uploads.
792792 const runtime = yield* options.makeRuntime({
793793 cwd,
794794 clientInfo: { name: "t3-code", version: "0.0.0" },
apps/server/src/provider/Layers/ProviderService.test.ts+51
......@@ -4801,6 +4801,57 @@ validation.layer("ProviderServiceLive validation", (it) => {
48014801 }),
48024802 );
48034803
4804 it.effect("rejects a file when its path cannot fit in the prompt", () =>
4805 Effect.gen(function* () {
4806 const provider = yield* ProviderService.ProviderService;
4807 validation.codex.sendTurn.mockClear();
4808 const failure = yield* provider
4809 .sendTurn({
4810 threadId: asThreadId("thread-file-path-context-limit"),
4811 input: "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS),
4812 attachments: [
4813 {
4814 type: "file",
4815 id: "thread-attach-12345678-1234-1234-1234-123456789abc-zip",
4816 name: "archive.zip",
4817 mimeType: "application/zip",
4818 sizeBytes: 1024,
4819 },
4820 ],
4821 })
4822 .pipe(Effect.flip);
4823
4824 assert.instanceOf(failure, ProviderValidationError);
4825 assert.include(failure.issue, String(PROVIDER_SEND_TURN_MAX_INPUT_CHARS));
4826 assert.equal(validation.codex.sendTurn.mock.calls.length, 0);
4827 }),
4828 );
4829
4830 it.effect("sends a native image when its path cannot fit in the prompt", () =>
4831 Effect.gen(function* () {
4832 const provider = yield* ProviderService.ProviderService;
4833 const threadId = asThreadId("thread-image-path-context-limit");
4834 yield* provider.startSession(threadId, {
4835 provider: ProviderDriverKind.make("codex"),
4836 providerInstanceId: codexInstanceId,
4837 threadId,
4838 runtimeMode: "full-access",
4839 });
4840 validation.codex.sendTurn.mockClear();
4841 const attachment = {
4842 type: "image" as const,
4843 id: "thread-attach-12345678-1234-1234-1234-123456789abc-png",
4844 name: "screen.png",
4845 mimeType: "image/png",
4846 sizeBytes: 1024,
4847 };
4848 const input = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
4849 yield* provider.sendTurn({ threadId, input, attachments: [attachment] });
4850 assert.equal(validation.codex.sendTurn.mock.calls[0]?.[0].input, input);
4851 assert.deepEqual(validation.codex.sendTurn.mock.calls[0]?.[0].attachments, [attachment]);
4852 }),
4853 );
4854
48044855 it.effect("rejects citation-expanded input over the provider character limit", () =>
48054856 Effect.gen(function* () {
48064857 const provider = yield* ProviderService.ProviderService;
apps/server/src/provider/Layers/ProviderService.ts+4-2
......@@ -1634,10 +1634,12 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
16341634 ? `[Pasted text "${attachment.name}" is saved at: ${attachmentPath}. Inspect it as needed.]`
16351635 : `[Attached ${attachment.type} "${attachment.name}" is saved at: ${attachmentPath}]`,
16361636 );
1637 if (isPastedText && !appended) {
1637 // Most adapters see generic files only through this path line, so a file
1638 // without one would be silently dropped. Images still go natively.
1639 if (!appended && attachment.type === "file") {
16381640 return yield* toValidationError(
16391641 "ProviderService.sendTurn",
1640 `Input plus pasted-text attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`,
1642 `Input plus attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`,
16411643 );
16421644 }
16431645 }
apps/server/src/provider/acp/AntigravityAcpSupport.test.ts+94-38
......@@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
22import { describe, expect, it } from "@effect/vitest";
33import {
44 ANTIGRAVITY_DEFAULT_MODEL,
5 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
65 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
76 type ChatAttachment,
87 type RuntimeMode,
......@@ -15,7 +14,6 @@ import type * as EffectAcpSchema from "effect-acp/schema";
1514
1615import { resolveAttachmentPath } from "../../attachmentStore.ts";
1716import {
18 ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES,
1917 antigravityPermissionMode,
2018 applyAntigravityAcpModelSelection,
2119 buildAntigravityPrompt,
......@@ -324,6 +322,41 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
324322 }),
325323 );
326324
325 it.effect.each([
326 { name: "archive.zip", mimeType: "application/zip" },
327 { name: "clip.mp4", mimeType: "video/mp4" },
328 { name: "recording.aiff", mimeType: "audio/aiff" },
329 { name: "large.pdf", mimeType: "application/pdf", sizeBytes: 75_000_000 },
330 { name: "large.txt", mimeType: "text/plain", sizeBytes: 1024 * 1024 + 1 },
331 { name: "large.wav", mimeType: "audio/wav", sizeBytes: 20 * 1024 * 1024 + 1 },
332 ])("keeps $name as a file path without reading or spending the native media budget", (file) =>
333 Effect.gen(function* () {
334 const fixture = yield* makeAttachmentFixture();
335 const attachment = { ...textAttachment, sizeBytes: 50 * 1024 * 1024, ...file };
336 const upload = yield* fixture.write(attachment, "");
337 yield* fixture.fs.truncate(upload.filePath, attachment.sizeBytes);
338 const pdf = yield* fixture.write(pdfAttachment, "");
339 yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024);
340 const input = `Inspect the file at ${upload.filePath}`;
341 const prompt = yield* buildAntigravityPrompt({
342 input,
343 attachments: [attachment, pdfAttachment],
344 attachmentsDir: fixture.attachmentsDir,
345 }).pipe(
346 Effect.provideService(FileSystem.FileSystem, {
347 ...fixture.fs,
348 stream: () => {
349 throw new Error("Path attachments must not be read into the prompt");
350 },
351 }),
352 );
353 expect(prompt).toEqual([
354 { type: "text", text: input },
355 { type: "resource_link", uri: pdf.uri, name: "report.pdf", mimeType: "application/pdf" },
356 ]);
357 }),
358 );
359
327360 it.effect("sends supported audio files as native audio content", () =>
328361 Effect.gen(function* () {
329362 const fixture = yield* makeAttachmentFixture();
......@@ -378,8 +411,6 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
378411
379412 it.effect.each([
380413 { ...imageAttachment, name: "animation.gif", mimeType: "image/gif" },
381 { ...textAttachment, name: "archive.zip", mimeType: "application/zip" },
382 { ...textAttachment, name: "recording.aiff", mimeType: "audio/aiff" },
383414 ] satisfies ReadonlyArray<ChatAttachment>)(
384415 "rejects $name instead of silently dropping it from a valid prompt",
385416 (attachment) =>
......@@ -400,32 +431,25 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
400431 }),
401432 );
402433
403 it.effect.each([
404 { attachment: textAttachment, bytes: ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES + 1 },
405 { attachment: imageAttachment, bytes: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1 },
406 { attachment: pdfAttachment, bytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 },
407 ])(
408 "rejects oversized $attachment.name using file size instead of upload metadata",
409 ({ attachment, bytes }) =>
410 Effect.gen(function* () {
411 const fixture = yield* makeAttachmentFixture();
412 const upload = yield* fixture.write(attachment, "");
413 yield* fixture.fs.truncate(upload.filePath, bytes);
414 const error = yield* buildAntigravityPrompt({
415 input: "Read this attachment.",
416 attachments: [attachment],
417 attachmentsDir: fixture.attachmentsDir,
418 }).pipe(Effect.flip);
419
420 expect(error).toMatchObject({
421 _tag: "AcpRequestError",
422 code: -32602,
423 errorMessage: expect.stringContaining(`'${attachment.name}' is too large`),
424 });
425 }),
434 it.effect("rejects oversized images using file size instead of upload metadata", () =>
435 Effect.gen(function* () {
436 const fixture = yield* makeAttachmentFixture();
437 const upload = yield* fixture.write(imageAttachment, "");
438 yield* fixture.fs.truncate(upload.filePath, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1);
439 const error = yield* buildAntigravityPrompt({
440 input: "Read this attachment.",
441 attachments: [imageAttachment],
442 attachmentsDir: fixture.attachmentsDir,
443 }).pipe(Effect.flip);
444 expect(error).toMatchObject({
445 _tag: "AcpRequestError",
446 code: -32602,
447 errorMessage: expect.stringContaining("'screen.png' is too large"),
448 });
449 }),
426450 );
427451
428 it.effect("accepts 50 MiB in total but rejects one byte more across files", () =>
452 it.effect("keeps PDF overflow on the file-path route", () =>
429453 Effect.gen(function* () {
430454 const fixture = yield* makeAttachmentFixture();
431455 const secondAttachment = {
......@@ -435,25 +459,57 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
435459 };
436460 const first = yield* fixture.write(pdfAttachment, "");
437461 const second = yield* fixture.write(secondAttachment, "");
438 yield* fixture.fs.truncate(first.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2);
439 yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2);
462 yield* fixture.fs.truncate(first.filePath, (50 * 1024 * 1024) / 2);
463 yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2);
440464 const input = {
441 input: undefined,
465 input: `Read ${first.filePath} and ${second.filePath}`,
442466 attachments: [pdfAttachment, secondAttachment],
443467 attachmentsDir: fixture.attachmentsDir,
444468 };
445469 const prompt = yield* buildAntigravityPrompt(input);
446470 expect(prompt).toEqual([
447 { type: "resource_link", uri: first.uri, name: "report.pdf", mimeType: "application/pdf" },
448 { type: "resource_link", uri: second.uri, name: "second.pdf", mimeType: "application/pdf" },
471 { type: "text", text: input.input },
472 {
473 type: "resource_link",
474 uri: first.uri,
475 name: "report.pdf",
476 mimeType: "application/pdf",
477 },
478 {
479 type: "resource_link",
480 uri: second.uri,
481 name: "second.pdf",
482 mimeType: "application/pdf",
483 },
449484 ]);
450485
451 yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2 + 1);
452 const error = yield* buildAntigravityPrompt(input).pipe(Effect.flip);
486 yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2 + 1);
487 expect(yield* buildAntigravityPrompt(input)).toEqual([
488 { type: "text", text: input.input },
489 {
490 type: "resource_link",
491 uri: first.uri,
492 name: "report.pdf",
493 mimeType: "application/pdf",
494 },
495 ]);
496 }),
497 );
498
499 it.effect("still rejects images when the native budget is full", () =>
500 Effect.gen(function* () {
501 const fixture = yield* makeAttachmentFixture();
502 const pdf = yield* fixture.write(pdfAttachment, "");
503 yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024);
504 yield* fixture.write(imageAttachment, new Uint8Array([1]));
505 const error = yield* buildAntigravityPrompt({
506 input: "Inspect both attachments.",
507 attachments: [pdfAttachment, imageAttachment],
508 attachmentsDir: fixture.attachmentsDir,
509 }).pipe(Effect.flip);
453510 expect(error).toMatchObject({
454511 _tag: "AcpRequestError",
455 code: -32602,
456 errorMessage: expect.stringContaining("'second.pdf' is too large"),
512 errorMessage: expect.stringContaining("'screen.png' is too large"),
457513 });
458514 }),
459515 );
......@@ -463,7 +519,7 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
463519 const fixture = yield* makeAttachmentFixture();
464520 const pdf = yield* fixture.write(pdfAttachment, "");
465521 const text = yield* fixture.write(textAttachment, "a");
466 yield* fixture.fs.truncate(pdf.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES - 1);
522 yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024 - 1);
467523 const error = yield* buildAntigravityPrompt({
468524 input: undefined,
469525 attachments: [pdfAttachment, textAttachment],
apps/server/src/provider/acp/AntigravityAcpSupport.ts+23-13
......@@ -243,10 +243,14 @@ const TEXT_FILE_EXTENSIONS = new Set([
243243 ".ini",
244244 ".conf",
245245]);
246export const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024;
246const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024;
247247const MAX_TOTAL_ATTACHMENT_BYTES = PROVIDER_SEND_TURN_MAX_FILE_BYTES;
248248
249/** Sends uploads as native ACP content instead of workspace path hints. */
249/**
250 * Sends supported uploads as native ACP content. Other files, and native
251 * candidates over their limits, reach the agent through the saved path
252 * ProviderService puts in the text block.
253 */
250254export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(function* (input: {
251255 readonly input: ProviderSendTurnInput["input"];
252256 readonly attachments: ProviderSendTurnInput["attachments"];
......@@ -280,7 +284,9 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi
280284 (mimeType.startsWith("text/") ||
281285 TEXT_MIME_TYPES.has(mimeType) ||
282286 TEXT_FILE_EXTENSIONS.has(path.extname(attachment.name).toLowerCase()));
283 if (!image && !audio && !pdf && !textFile) {
287 const isPathOnly =
288 attachment.type === "file" && (isPastedText || (!audio && !pdf && !textFile));
289 if (attachment.type === "image" && !image) {
284290 return yield* EffectAcpErrors.AcpRequestError.invalidParams(
285291 `Antigravity does not support '${attachment.name}' (${attachment.mimeType}). Attach a BMP, JPEG, PNG, WebP, PDF, audio, or text file.`,
286292 );
......@@ -303,26 +309,30 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi
303309 ),
304310 ),
305311 );
306 if (isPastedText) {
307 if (info.type !== "File") {
308 return yield* EffectAcpErrors.AcpRequestError.invalidParams(
309 `Could not read attachment '${attachment.name}'.`,
310 );
311 }
312 continue;
312 if (info.type !== "File") {
313 return yield* EffectAcpErrors.AcpRequestError.invalidParams(
314 `Could not read attachment '${attachment.name}'.`,
315 );
313316 }
317 if (isPathOnly) continue;
314318 const size = Number(info.size);
315319 const limit = image
316320 ? PROVIDER_SEND_TURN_MAX_IMAGE_BYTES
317321 : audio
318322 ? ANTIGRAVITY_MAX_AUDIO_ATTACHMENT_BYTES
319323 : pdf
320 ? PROVIDER_SEND_TURN_MAX_FILE_BYTES
324 ? MAX_TOTAL_ATTACHMENT_BYTES
321325 : ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES;
326 if (
327 attachment.type === "file" &&
328 (size > limit || totalBytes + size > MAX_TOTAL_ATTACHMENT_BYTES)
329 ) {
330 continue;
331 }
322332 totalBytes += size;
323 if (info.type !== "File" || size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
333 if (size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
324334 return yield* EffectAcpErrors.AcpRequestError.invalidParams(
325 `Attachment '${attachment.name}' is too large. Antigravity accepts text files up to 1 MiB, images up to 10 MiB, audio up to 20 MiB, and 50 MiB total attachments.`,
335 `Image '${attachment.name}' is too large. Antigravity accepts images up to 10 MiB and 50 MiB of native attachments per message.`,
326336 );
327337 }
328338 const uri = yield* path.toFileUrl(attachmentPath).pipe(
docs/user/composer.md+1-2
......@@ -19,8 +19,7 @@ Attach up to 100 files per message. Each image can be up to 10 MiB, with at most
191950 MiB each, subject to the environment's upload support and limit. The agent
2020receives them on the environment's machine. Provider and model limits still
2121apply, including images already in the conversation. A video attachment gives
22the agent a file path; it does not enable native video input. Antigravity does
23not accept video attachments.
22the agent a file path; it does not enable native video input.
2423
2524Uploads begin when you add an attachment. All uploads must finish before the
2625message can send. Retry or remove a failed upload. On web and desktop, reloading
docs/user/providers-antigravity.md+6-3
......@@ -94,9 +94,12 @@ so a skill there only appears when the project itself is your home directory.
9494
9595Antigravity accepts images, PDFs, text files, and supported audio formats directly.
9696Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and
9750 MiB total attachments per message. Unsupported formats are rejected. These
98limits can be lower than the general upload limit; uploading a file does not
99mean this provider can use it.
9750 MiB total native attachments per message. PDFs, text, and audio that exceed
98their native limits or the remaining native budget are passed as file paths
99for the agent to inspect with tools. ZIP archives and videos also use file paths.
100These files do not count toward the native attachment budget or enable native video input.
101Unsupported image formats are rejected. The native limits can be lower than
102the general upload limit.
100103
101104### Subagents
102105