Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/server/src/provider/Layers/AntigravityAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,9 +786,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
stopOwned,
Effect.gen(function* () {
const mcp = McpProviderSession.readMcpProviderSession(input.threadId);
// The attachments dir grant lets the agent read pasted files at
// the paths ProviderService injects into the turn text. It is a
// leaf directory holding only uploads.
// The attachments dir grant lets the agent read path-only uploads
// at the paths ProviderService injects into the turn text. It is
// a leaf directory holding only uploads.
const runtime = yield* options.makeRuntime({
cwd,
clientInfo: { name: "t3-code", version: "0.0.0" },
Expand Down
51 changes: 51 additions & 0 deletions apps/server/src/provider/Layers/ProviderService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4729,6 +4729,57 @@ validation.layer("ProviderServiceLive validation", (it) => {
}),
);

it.effect("rejects a file when its path cannot fit in the prompt", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;
validation.codex.sendTurn.mockClear();
const failure = yield* provider
.sendTurn({
threadId: asThreadId("thread-file-path-context-limit"),
input: "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS),
attachments: [
{
type: "file",
id: "thread-attach-12345678-1234-1234-1234-123456789abc-zip",
name: "archive.zip",
mimeType: "application/zip",
sizeBytes: 1024,
},
],
})
.pipe(Effect.flip);

assert.instanceOf(failure, ProviderValidationError);
assert.include(failure.issue, String(PROVIDER_SEND_TURN_MAX_INPUT_CHARS));
assert.equal(validation.codex.sendTurn.mock.calls.length, 0);
}),
);

it.effect("sends a native image when its path cannot fit in the prompt", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;
const threadId = asThreadId("thread-image-path-context-limit");
yield* provider.startSession(threadId, {
provider: ProviderDriverKind.make("codex"),
providerInstanceId: codexInstanceId,
threadId,
runtimeMode: "full-access",
});
validation.codex.sendTurn.mockClear();
const attachment = {
type: "image" as const,
id: "thread-attach-12345678-1234-1234-1234-123456789abc-png",
name: "screen.png",
mimeType: "image/png",
sizeBytes: 1024,
};
const input = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
yield* provider.sendTurn({ threadId, input, attachments: [attachment] });
assert.equal(validation.codex.sendTurn.mock.calls[0]?.[0].input, input);
assert.deepEqual(validation.codex.sendTurn.mock.calls[0]?.[0].attachments, [attachment]);
}),
);

it.effect("rejects citation-expanded input over the provider character limit", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/provider/Layers/ProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1626,10 +1626,12 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
? `[Pasted text "${attachment.name}" is saved at: ${attachmentPath}. Inspect it as needed.]`
: `[Attached ${attachment.type} "${attachment.name}" is saved at: ${attachmentPath}]`,
);
if (isPastedText && !appended) {
// Most adapters see generic files only through this path line, so a file
// without one would be silently dropped. Images still go natively.
if (!appended && attachment.type === "file") {
return yield* toValidationError(
"ProviderService.sendTurn",
`Input plus pasted-text attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`,
`Input plus attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`,
);
}
}
Expand Down
132 changes: 94 additions & 38 deletions apps/server/src/provider/acp/AntigravityAcpSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import {
ANTIGRAVITY_DEFAULT_MODEL,
PROVIDER_SEND_TURN_MAX_FILE_BYTES,
PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
type ChatAttachment,
type RuntimeMode,
Expand All @@ -15,7 +14,6 @@ import type * as EffectAcpSchema from "effect-acp/schema";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import {
ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES,
antigravityPermissionMode,
applyAntigravityAcpModelSelection,
buildAntigravityPrompt,
Expand Down Expand Up @@ -324,6 +322,41 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
}),
);

it.effect.each([
{ name: "archive.zip", mimeType: "application/zip" },
{ name: "clip.mp4", mimeType: "video/mp4" },
{ name: "recording.aiff", mimeType: "audio/aiff" },
{ name: "large.pdf", mimeType: "application/pdf", sizeBytes: 75_000_000 },
{ name: "large.txt", mimeType: "text/plain", sizeBytes: 1024 * 1024 + 1 },
{ name: "large.wav", mimeType: "audio/wav", sizeBytes: 20 * 1024 * 1024 + 1 },
])("keeps $name as a file path without reading or spending the native media budget", (file) =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
const attachment = { ...textAttachment, sizeBytes: 50 * 1024 * 1024, ...file };
const upload = yield* fixture.write(attachment, "");
yield* fixture.fs.truncate(upload.filePath, attachment.sizeBytes);
const pdf = yield* fixture.write(pdfAttachment, "");
yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024);
const input = `Inspect the file at ${upload.filePath}`;
const prompt = yield* buildAntigravityPrompt({
input,
attachments: [attachment, pdfAttachment],
attachmentsDir: fixture.attachmentsDir,
}).pipe(
Effect.provideService(FileSystem.FileSystem, {
...fixture.fs,
stream: () => {
throw new Error("Path attachments must not be read into the prompt");
},
}),
);
expect(prompt).toEqual([
{ type: "text", text: input },
{ type: "resource_link", uri: pdf.uri, name: "report.pdf", mimeType: "application/pdf" },
]);
}),
);

it.effect("sends supported audio files as native audio content", () =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
Expand Down Expand Up @@ -378,8 +411,6 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {

it.effect.each([
{ ...imageAttachment, name: "animation.gif", mimeType: "image/gif" },
{ ...textAttachment, name: "archive.zip", mimeType: "application/zip" },
{ ...textAttachment, name: "recording.aiff", mimeType: "audio/aiff" },
] satisfies ReadonlyArray<ChatAttachment>)(
"rejects $name instead of silently dropping it from a valid prompt",
(attachment) =>
Expand All @@ -400,32 +431,25 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
}),
);

it.effect.each([
{ attachment: textAttachment, bytes: ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES + 1 },
{ attachment: imageAttachment, bytes: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1 },
{ attachment: pdfAttachment, bytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 },
])(
"rejects oversized $attachment.name using file size instead of upload metadata",
({ attachment, bytes }) =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
const upload = yield* fixture.write(attachment, "");
yield* fixture.fs.truncate(upload.filePath, bytes);
const error = yield* buildAntigravityPrompt({
input: "Read this attachment.",
attachments: [attachment],
attachmentsDir: fixture.attachmentsDir,
}).pipe(Effect.flip);

expect(error).toMatchObject({
_tag: "AcpRequestError",
code: -32602,
errorMessage: expect.stringContaining(`'${attachment.name}' is too large`),
});
}),
it.effect("rejects oversized images using file size instead of upload metadata", () =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
const upload = yield* fixture.write(imageAttachment, "");
yield* fixture.fs.truncate(upload.filePath, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1);
const error = yield* buildAntigravityPrompt({
input: "Read this attachment.",
attachments: [imageAttachment],
attachmentsDir: fixture.attachmentsDir,
}).pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "AcpRequestError",
code: -32602,
errorMessage: expect.stringContaining("'screen.png' is too large"),
});
}),
);

it.effect("accepts 50 MiB in total but rejects one byte more across files", () =>
it.effect("keeps PDF overflow on the file-path route", () =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
const secondAttachment = {
Expand All @@ -435,25 +459,57 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
};
const first = yield* fixture.write(pdfAttachment, "");
const second = yield* fixture.write(secondAttachment, "");
yield* fixture.fs.truncate(first.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2);
yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2);
yield* fixture.fs.truncate(first.filePath, (50 * 1024 * 1024) / 2);
yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2);
const input = {
input: undefined,
input: `Read ${first.filePath} and ${second.filePath}`,
attachments: [pdfAttachment, secondAttachment],
attachmentsDir: fixture.attachmentsDir,
};
const prompt = yield* buildAntigravityPrompt(input);
expect(prompt).toEqual([
{ type: "resource_link", uri: first.uri, name: "report.pdf", mimeType: "application/pdf" },
{ type: "resource_link", uri: second.uri, name: "second.pdf", mimeType: "application/pdf" },
{ type: "text", text: input.input },
{
type: "resource_link",
uri: first.uri,
name: "report.pdf",
mimeType: "application/pdf",
},
{
type: "resource_link",
uri: second.uri,
name: "second.pdf",
mimeType: "application/pdf",
},
]);

yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2 + 1);
const error = yield* buildAntigravityPrompt(input).pipe(Effect.flip);
yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2 + 1);
expect(yield* buildAntigravityPrompt(input)).toEqual([
{ type: "text", text: input.input },
{
type: "resource_link",
uri: first.uri,
name: "report.pdf",
mimeType: "application/pdf",
},
]);
}),
);

it.effect("still rejects images when the native budget is full", () =>
Effect.gen(function* () {
const fixture = yield* makeAttachmentFixture();
const pdf = yield* fixture.write(pdfAttachment, "");
yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024);
yield* fixture.write(imageAttachment, new Uint8Array([1]));
const error = yield* buildAntigravityPrompt({
input: "Inspect both attachments.",
attachments: [pdfAttachment, imageAttachment],
attachmentsDir: fixture.attachmentsDir,
}).pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "AcpRequestError",
code: -32602,
errorMessage: expect.stringContaining("'second.pdf' is too large"),
errorMessage: expect.stringContaining("'screen.png' is too large"),
});
}),
);
Expand All @@ -463,7 +519,7 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => {
const fixture = yield* makeAttachmentFixture();
const pdf = yield* fixture.write(pdfAttachment, "");
const text = yield* fixture.write(textAttachment, "a");
yield* fixture.fs.truncate(pdf.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES - 1);
yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024 - 1);
const error = yield* buildAntigravityPrompt({
input: undefined,
attachments: [pdfAttachment, textAttachment],
Expand Down
36 changes: 23 additions & 13 deletions apps/server/src/provider/acp/AntigravityAcpSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,14 @@ const TEXT_FILE_EXTENSIONS = new Set([
".ini",
".conf",
]);
export const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024;
const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024;
const MAX_TOTAL_ATTACHMENT_BYTES = PROVIDER_SEND_TURN_MAX_FILE_BYTES;

/** Sends uploads as native ACP content instead of workspace path hints. */
/**
* Sends supported uploads as native ACP content. Other files, and native
* candidates over their limits, reach the agent through the saved path
* ProviderService puts in the text block.
*/
export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(function* (input: {
readonly input: ProviderSendTurnInput["input"];
readonly attachments: ProviderSendTurnInput["attachments"];
Expand Down Expand Up @@ -280,7 +284,9 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi
(mimeType.startsWith("text/") ||
TEXT_MIME_TYPES.has(mimeType) ||
TEXT_FILE_EXTENSIONS.has(path.extname(attachment.name).toLowerCase()));
if (!image && !audio && !pdf && !textFile) {
const isPathOnly =
attachment.type === "file" && (isPastedText || (!audio && !pdf && !textFile));
if (attachment.type === "image" && !image) {
return yield* EffectAcpErrors.AcpRequestError.invalidParams(
`Antigravity does not support '${attachment.name}' (${attachment.mimeType}). Attach a BMP, JPEG, PNG, WebP, PDF, audio, or text file.`,
);
Expand All @@ -303,26 +309,30 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi
),
),
);
if (isPastedText) {
if (info.type !== "File") {
return yield* EffectAcpErrors.AcpRequestError.invalidParams(
`Could not read attachment '${attachment.name}'.`,
);
}
continue;
if (info.type !== "File") {
return yield* EffectAcpErrors.AcpRequestError.invalidParams(
`Could not read attachment '${attachment.name}'.`,
);
}
if (isPathOnly) continue;
const size = Number(info.size);
const limit = image
? PROVIDER_SEND_TURN_MAX_IMAGE_BYTES
: audio
? ANTIGRAVITY_MAX_AUDIO_ATTACHMENT_BYTES
: pdf
? PROVIDER_SEND_TURN_MAX_FILE_BYTES
? MAX_TOTAL_ATTACHMENT_BYTES
: ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES;
if (
attachment.type === "file" &&
(size > limit || totalBytes + size > MAX_TOTAL_ATTACHMENT_BYTES)
) {
continue;
}
totalBytes += size;
if (info.type !== "File" || size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
if (size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
return yield* EffectAcpErrors.AcpRequestError.invalidParams(
`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.`,
`Image '${attachment.name}' is too large. Antigravity accepts images up to 10 MiB and 50 MiB of native attachments per message.`,
);
}
const uri = yield* path.toFileUrl(attachmentPath).pipe(
Expand Down
3 changes: 1 addition & 2 deletions docs/user/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ Attach up to 100 files per message. Each image can be up to 10 MiB, with at most
50 MiB each, subject to the environment's upload support and limit. The agent
receives them on the environment's machine. Provider and model limits still
apply, including images already in the conversation. A video attachment gives
the agent a file path; it does not enable native video input. Antigravity does
not accept video attachments.
the agent a file path; it does not enable native video input.

Uploads begin when you add an attachment. All uploads must finish before the
message can send. Retry or remove a failed upload. On web and desktop, reloading
Expand Down
9 changes: 6 additions & 3 deletions docs/user/providers-antigravity.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ so a skill there only appears when the project itself is your home directory.

Antigravity accepts images, PDFs, text files, and supported audio formats directly.
Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and
50 MiB total attachments per message. Unsupported formats are rejected. These
limits can be lower than the general upload limit; uploading a file does not
mean this provider can use it.
50 MiB total native attachments per message. PDFs, text, and audio that exceed
their native limits or the remaining native budget are passed as file paths
for the agent to inspect with tools. ZIP archives and videos also use file paths.
These files do not count toward the native attachment budget or enable native video input.
Unsupported image formats are rejected. The native limits can be lower than
the general upload limit.

### Subagents

Expand Down
Loading