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
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const layerSession = Layer.mergeAll(
);

describe("AntigravityAdapterV2 client file system", () => {
it.effect("confines agent file requests to the workspace under full access", () =>
it.effect("serves confined reads and leaves edits to the agent's diff tools", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
Expand All @@ -140,6 +140,8 @@ describe("AntigravityAdapterV2 client file system", () => {
// effect-acp keeps the last handler registered per method; so does this.
let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
let writeTextFile: Parameters<RuntimeService["handleWriteTextFile"]>[0] | undefined;
const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" });
const requestLog = path.join(logs, "requests.ndjson");
const crypto = yield* Crypto.Crypto;
const instanceId = ProviderInstanceId.make("antigravity-containment-test");
const adapter = yield* makeAntigravityAdapterV2({
Expand All @@ -153,7 +155,7 @@ describe("AntigravityAdapterV2 client file system", () => {
command: process.execPath,
args: [mockAgentPath],
cwd: input.cwd,
env: { T3_ACP_ANTIGRAVITY: "1" },
env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_REQUEST_LOG_PATH: requestLog },
},
}).pipe(
Effect.provideService(Crypto.Crypto, crypto),
Expand Down Expand Up @@ -198,17 +200,31 @@ describe("AntigravityAdapterV2 client file system", () => {
runtimePolicy,
});
yield* session.ensureThread({ threadId, modelSelection, runtimePolicy });
if (readTextFile === undefined || writeTextFile === undefined) {
return yield* Effect.die("Antigravity sessions must serve client file requests");
const initialize = (yield* fileSystem.readFileString(requestLog))
.trim()
.split("\n")
.map(
(line) =>
JSON.parse(line) as {
method?: string;
params?: { clientCapabilities?: { fs?: unknown }; clientInfo?: { name?: string } };
},
)
.find((request) => request.method === "initialize");
assert.deepEqual(initialize?.params?.clientCapabilities?.fs, {
readTextFile: true,
writeTextFile: false,
});
assert.equal(initialize?.params?.clientInfo?.name, "zed");
assert.isUndefined(writeTextFile);
if (readTextFile === undefined) {
return yield* Effect.die("Antigravity sessions must serve client file reads");
}
const context = (method: string) => ({ requestId: `test-${method}`, method });

const insidePath = path.join(workspace, "src", "inside.ts");
yield* writeTextFile(
{ sessionId: "mock-session-1", path: insidePath, content: "inside" },
context("fs/write_text_file"),
);
assert.equal(yield* fileSystem.readFileString(insidePath), "inside");
yield* fileSystem.makeDirectory(path.dirname(insidePath), { recursive: true });
yield* fileSystem.writeFileString(insidePath, "inside");
const pasted = yield* readTextFile(
{ sessionId: "mock-session-1", path: attachment },
context("fs/read_text_file"),
Expand All @@ -220,59 +236,13 @@ describe("AntigravityAdapterV2 client file system", () => {
context("fs/read_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(outsideRead));
const outsideWrite = yield* writeTextFile(
{ sessionId: "mock-session-1", path: path.join(outside, "planted.txt"), content: "x" },
context("fs/write_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(outsideWrite));
assert.isFalse(yield* fileSystem.exists(path.join(outside, "planted.txt")));

const linkPath = path.join(workspace, "linked-secret.txt");
yield* fileSystem.symlink(outsideFile, linkPath);
const linkedRead = yield* readTextFile(
{ sessionId: "mock-session-1", path: linkPath },
context("fs/read_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(linkedRead), "a read through an escaping symlink is denied");
const linkedWrite = yield* writeTextFile(
{ sessionId: "mock-session-1", path: linkPath, content: "overwritten" },
context("fs/write_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(linkedWrite), "a write through an escaping symlink is denied");
assert.equal(yield* fileSystem.readFileString(outsideFile), "secret");
const plantedTarget = path.join(outside, "created-through-link.txt");
const danglingLink = path.join(workspace, "dangling.txt");
yield* fileSystem.symlink(plantedTarget, danglingLink);
const danglingWrite = yield* writeTextFile(
{ sessionId: "mock-session-1", path: danglingLink, content: "planted" },
context("fs/write_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(danglingWrite), "a write through a dangling symlink is denied");
assert.isFalse(yield* fileSystem.exists(plantedTarget));
yield* fileSystem.symlink(outside, path.join(workspace, "linked-dir"));
const linkedDirWrite = yield* writeTextFile(
{
sessionId: "mock-session-1",
path: path.join(workspace, "linked-dir", "new.txt"),
content: "planted",
},
context("fs/write_text_file"),
).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(linkedDirWrite), "a write under an escaping directory link");
assert.isFalse(yield* fileSystem.exists(path.join(outside, "new.txt")));
const nestedLinkedDirWrite = yield* writeTextFile(
{
sessionId: "mock-session-1",
path: path.join(workspace, "linked-dir", "a", "b", "file.txt"),
content: "planted",
},
context("fs/write_text_file"),
).pipe(Effect.exit);
assert.isTrue(
Exit.isFailure(nestedLinkedDirWrite),
"a write to missing directories under an escaping directory link",
);
assert.isFalse(yield* fileSystem.exists(path.join(outside, "a")));
const insideLink = path.join(workspace, "inside-link.ts");
yield* fileSystem.symlink(insidePath, insideLink);
const viaInsideLink = yield* readTextFile(
Expand Down
16 changes: 3 additions & 13 deletions apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ import {
buildAntigravityPrompt,
type AntigravityAcpRuntimeInput,
} from "../../provider/acp/AntigravityAcpSupport.ts";
import {
readAntigravityClientTextFile,
writeAntigravityClientTextFile,
} from "../../provider/acp/AntigravityClientFiles.ts";
import { readAntigravityClientTextFile } from "../../provider/acp/AntigravityClientFiles.ts";
import {
antigravityApprovalOptions,
antigravitySubagentOutput,
Expand Down Expand Up @@ -82,7 +79,7 @@ export interface AntigravityAdapterV2Options {
readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"];
}

/** ACP 1.1.1 reports `start_subagent` tools without child session ids. */
/** Native subagent tools report no child session ids. */
const extractAntigravitySubagentUpdate: NonNullable<AcpAdapterV2Flavor["extractSubagentUpdate"]> = (
toolCall,
) => {
Expand Down Expand Up @@ -126,7 +123,7 @@ export function makeAntigravityAcpAdapterFlavor(
Scope.close(scope, Exit.void),
options.makeRuntime({
...input,
clientFileSystem: true,
clientFileReads: true,
additionalDirectories: [options.host.paths.attachmentsDir],
}),
)
Expand Down Expand Up @@ -191,13 +188,6 @@ export function makeAntigravityAcpAdapterFlavor(
allowedRoots: antigravityClientFileRoots(cwd),
request,
}),
writeTextFile: (request, cwd) =>
writeAntigravityClientTextFile({
fileSystem: options.fileSystem,
path: options.path,
allowedRoots: antigravityClientFileRoots(cwd),
request,
}),
},
approvalOptions: antigravityApprovalOptions,
extractPermissionQuestion: (request) => {
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/provider/AntigravityInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,6 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
}),
)
: spawner,
clientInfo: { name: "t3-code", version: "0.0.0" },
});
const initialized = yield* runtime.initialize();
if (
Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/provider/AntigravityProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,50 @@ describe("Antigravity model catalog", () => {
"gemini-pro-agent",
);
});

it("offers each model the thinking levels it reported when selected", () => {
const thinking = (currentValue: string) =>
({
id: "thinking_level",
name: "Thinking Level",
category: "thought_level",
type: "select",
currentValue,
options: [
{ value: "low", name: "Low" },
{ value: "high", name: "High" },
],
}) satisfies EffectAcpSchema.SessionConfigOption;
const models = buildAntigravityModelsFromSession(
{
configOptions: [
{
...modelConfig,
currentValue: "gemini",
options: [
{ value: "gemini", name: "Gemini" },
{ value: "claude", name: "Claude" },
{ value: "plain", name: "Plain" },
{ value: "unseen", name: "Unseen" },
],
},
thinking("high"),
],
},
new Map([
["claude", thinking("low")],
["plain", null],
]),
);
const thinkingOf = (slug: string) =>
models.find((model) => model.slug === slug)?.capabilities?.optionDescriptors?.[0];

expect(thinkingOf("gemini")).toMatchObject({ id: "thinking_level", currentValue: "high" });
expect(thinkingOf("claude")).toMatchObject({ id: "thinking_level", currentValue: "low" });
expect(thinkingOf("plain")).toBeUndefined();
expect(thinkingOf("unseen")).toMatchObject({ id: "thinking_level" });
expect(thinkingOf("unseen")).not.toHaveProperty("currentValue");
});
});

it.layer(layerTest)("Antigravity provider snapshots", (it) => {
Expand Down
45 changes: 42 additions & 3 deletions apps/server/src/provider/AntigravityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type * as EffectAcpErrors from "effect-acp/errors";
import type * as EffectAcpSchema from "effect-acp/compat";

import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
import { acpProviderOptionDescriptors } from "@t3tools/provider-acp/server/sessionConfig";
import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
import {
makeManualOnlyProviderMaintenanceCapabilities,
Expand All @@ -32,6 +33,8 @@ import {
} from "@t3tools/provider-core/server/snapshotProbe";
import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits";

import { antigravityThinkingConfig } from "./acp/AntigravityAcpSupport.ts";

const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
const MAX_WORKSPACE_SNAPSHOTS = 32;
const HEALTH_CHECK_TIMEOUT = "90 seconds";
Expand All @@ -44,8 +47,24 @@ type SessionSetupResult = Pick<
"configOptions" | "models"
>;

type ThinkingConfig = NonNullable<ReturnType<typeof antigravityThinkingConfig>>;

/** Thinking options each model reported when selected; `null` means it has none. */
export type AntigravityThinkingLevels = ReadonlyMap<string, ThinkingConfig | null>;

function thinkingCapabilities(config: ThinkingConfig | null | undefined, observed: boolean) {
const descriptor = config
? acpProviderOptionDescriptors({ configOptions: [config], modeState: undefined })[0]
: undefined;
if (descriptor?.type !== "select") return EMPTY_MODEL_CAPABILITIES;
// Another model's selected level is not this model's default.
const { currentValue: _currentValue, ...unobserved } = descriptor;
return createModelCapabilities({ optionDescriptors: [observed ? descriptor : unobserved] });
}

export function buildAntigravityModelsFromSession(
setup: SessionSetupResult,
thinkingLevels: AntigravityThinkingLevels = new Map(),
): ReadonlyArray<ServerProviderModel> {
const config = setup.configOptions?.find(
(option) => option.id === "model" || option.category === "model",
Expand All @@ -61,10 +80,13 @@ export function buildAntigravityModelsFromSession(
name: model.name,
})) ?? [])
: [];
const currentThinking = antigravityThinkingConfig(setup.configOptions);
const seen = new Set<string>();
return entries.flatMap((entry): ServerProviderModel[] => {
if (!entry.value.trim() || seen.has(entry.value)) return [];
seen.add(entry.value);
const observed =
entry.value === currentValue ? (currentThinking ?? null) : thinkingLevels.get(entry.value);
return [
{
slug: entry.value,
Expand All @@ -73,7 +95,10 @@ export function buildAntigravityModelsFromSession(
...(entry.value === currentValue
? { isDefault: true, aliases: [ANTIGRAVITY_DEFAULT_MODEL] }
: {}),
capabilities: EMPTY_MODEL_CAPABILITIES,
capabilities:
observed === undefined
? thinkingCapabilities(currentThinking, false)
: thinkingCapabilities(observed, true),
},
];
});
Expand Down Expand Up @@ -162,6 +187,17 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
});
// Preserve disk-discovered skills when callbacks replace workspace commands; the registry will not rescan them.
const discoveredSkills = new Map<string, ServerProvider["skills"]>();
const thinkingLevels = new Map<string, ThinkingConfig | null>();
const observeThinkingLevels = (
configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined,
) => {
const model = configOptions?.find(
(option) => option.id === "model" || option.category === "model",
);
if (model?.type === "select" && model.currentValue) {
thinkingLevels.set(model.currentValue, antigravityThinkingConfig(configOptions) ?? null);
}
};
const getSnapshot = SubscriptionRef.get(metadata).pipe(
Effect.flatMap((state) => options.stampIdentity(state.draft)),
);
Expand Down Expand Up @@ -267,6 +303,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
cwd?: string,
) {
if (!settings.enabled) return;
observeThinkingLevels(started.sessionSetupResult.configOptions);
const before = yield* SubscriptionRef.get(metadata);
const supportsTextGeneration = yield* options.supportsTextGeneration;
const usageLimits =
Expand Down Expand Up @@ -301,7 +338,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
published: draft.usageLimits,
probed: usageLimits ?? draft.usageLimits,
}),
models: buildAntigravityModelsFromSession(started.sessionSetupResult),
models: buildAntigravityModelsFromSession(started.sessionSetupResult, thinkingLevels),
supportsTextGeneration,
...(cwd
? {
Expand All @@ -324,7 +361,8 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
const onConfigOptionsUpdated = Effect.fn("AntigravityProvider.onConfigOptionsUpdated")(function* (
configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
) {
const models = buildAntigravityModelsFromSession({ configOptions });
observeThinkingLevels(configOptions);
const models = buildAntigravityModelsFromSession({ configOptions }, thinkingLevels);
yield* SubscriptionRef.update(metadata, (state) => {
if (state.draft.auth.status !== "authenticated") return state;
return { ...state, draft: { ...state.draft, models } };
Expand Down Expand Up @@ -391,6 +429,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
}) satisfies AntigravityProviderState,
);
discoveredSkills.clear();
thinkingLevels.clear();
});

const snapshotForCwd = Effect.fn("AntigravityProvider.snapshotForCwd")(function* (
Expand Down
Loading
Loading