Skip to content
Closed
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
46 changes: 46 additions & 0 deletions apps/server/src/provider/AntigravityInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
});
expect(yield* installation.resolve(externalExecutable)).toMatchObject({
executablePath: externalExecutable,
resolvedCommandPath: externalExecutable,
source: "override",
managedVersionDirectory: null,
});
Expand Down Expand Up @@ -768,13 +769,58 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
).toMatchObject({
source: "path",
executablePath: externalExecutable,
resolvedCommandPath: externalExecutable,
});
expect(
yield* isolated.installation.resolve(executableName, { PATH: externalDirectory }),
).toMatchObject({ source: "override", executablePath: externalExecutable });
}),
);

it.effect.skipIf(hostPlatform !== "win32")(
"preserves a selected Scoop alias while resolving its paired runtime",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-scoop-test-" });
const scoopRoot = path.join(baseDir, "Scoop Root");
const current = path.join(scoopRoot, "apps", "fixture-antigravity-runtime", "current");
const shims = path.join(scoopRoot, "shims");
const executable = path.join(current, executableName);
const harness = path.join(current, harnessName);
const alias = path.join(shims, executableName);
yield* fs.makeDirectory(current, { recursive: true });
yield* fs.makeDirectory(shims, { recursive: true });
yield* fs.writeFileString(executable, serverContents);
yield* fs.writeFileString(harness, harnessContents);
yield* fs.writeFileString(alias, "scoop shim");
yield* fs.writeFileString(alias.replace(/\.exe$/iu, ".shim"), `path = "${executable}"\r\n`);
const { installation } = yield* makeHarness({ baseDir });

expect(yield* installation.resolve(alias)).toMatchObject({
executablePath: executable,
resolvedCommandPath: alias,
harnessPath: harness,
source: "override",
managedVersionDirectory: null,
});
}),
);

it.effect("recognizes an explicitly selected managed release by directory identity", () =>
Effect.gen(function* () {
const { installation } = yield* makeHarness({ previous: true });
const managed = yield* installation.resolve();
expect(yield* installation.resolve(managed.executablePath)).toMatchObject({
executablePath: managed.executablePath,
resolvedCommandPath: managed.executablePath,
source: "override",
managedVersionDirectory: managed.managedVersionDirectory,
});
}),
);

it.effect("keeps leased releases available while new sessions resolve the new release", () =>
Effect.gen(function* () {
const { installation, fs, stagingReleased } = yield* makeHarness({ previous: true });
Expand Down
29 changes: 27 additions & 2 deletions apps/server/src/provider/AntigravityInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const isInstallationError = Schema.is(AntigravityInstallationError);

export interface AntigravityExecutable {
readonly executablePath: string;
/** Selected alias before realpath resolution, when an external install supplied one. */
readonly resolvedCommandPath?: string;
readonly harnessPath: string;
readonly source: "override" | "managed" | "path";
readonly version: string | null;
Expand Down Expand Up @@ -367,7 +369,23 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
source: "override" | "path",
) {
if (!(yield* executableFile(candidate))) return null;
const executablePath = yield* fs.realPath(candidate);
let executablePath = yield* fs.realPath(candidate);
if (platform === "win32" && /(?:^|[\\/])shims[\\/][^\\/]+\.exe$/iu.test(candidate)) {
const shimPath = candidate.replace(/\.exe$/iu, ".shim");
const shimInfo = yield* fs.stat(shimPath).pipe(Effect.option);
if (
Option.isSome(shimInfo) &&
shimInfo.value.type === "File" &&
Number(shimInfo.value.size) <= RECORD_MAX_BYTES
) {
const contents = yield* fs.readFileString(shimPath);
const targets = [...contents.matchAll(/^\s*path\s*=\s*"([^"\r\n]+)"\s*$/gimu)];
const target = targets.length === 1 ? targets[0]![1]! : null;
if (target && path.isAbsolute(target) && (yield* executableFile(target))) {
executablePath = yield* fs.realPath(target);
}
}
}
const directory = path.dirname(executablePath);
const harnessPath = path.join(directory, names.harness);
if (!(yield* executableFile(harnessPath))) return null;
Expand All @@ -378,10 +396,17 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
/^[a-f0-9]{64}$/u.test(path.basename(directory))
) {
const installed = yield* completedRelease(path.basename(directory));
return { ...installed, executablePath, harnessPath, source } satisfies AntigravityExecutable;
return {
...installed,
executablePath,
resolvedCommandPath: candidate,
harnessPath,
source,
} satisfies AntigravityExecutable;
}
return {
executablePath,
resolvedCommandPath: candidate,
harnessPath,
source,
version: null,
Expand Down
86 changes: 83 additions & 3 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ function shellQuote(value: string): string {
}

const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
options: { readonly config?: Partial<AntigravitySettings>; readonly enabled?: boolean } = {},
options: {
readonly config?: Partial<AntigravitySettings>;
readonly enabled?: boolean;
readonly externalScoop?: boolean;
} = {},
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand All @@ -75,7 +79,9 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
);
const requestLog = path.join(root, "requests.jsonl");
const profileDirectory = resolveAntigravityProfileDirectory(config.stateDir, instanceId);
const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`;
let instancePath = [path.join(root, "instance-bin"), baseEnv.PATH ?? ""]
.filter(Boolean)
.join(windowsHost ? ";" : ":");

const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* (
name: string,
Expand Down Expand Up @@ -113,7 +119,37 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
const first = yield* makeExecutable("runtime 'one");
const second = yield* makeExecutable("runtime two");
const signedOut = yield* makeExecutable("runtime signed-out", true);
const controls = { selected: first, failResolution: false, beforeAcquire: Effect.void };
let selected: AntigravityExecutable = first;
if (options.externalScoop) {
const scoopRoot = path.join(root, "Scoop Root");
const app = "fixture-antigravity-runtime";
const current = path.join(scoopRoot, "apps", app, "current");
const executablePath = path.join(current, "agy_acp_server.exe");
const harnessPath = path.join(current, "localharness_external.exe");
const manager = path.join(scoopRoot, "shims", "scoop.cmd");
const selectedAlias = path.join(scoopRoot, "shims", "agy_acp_server.exe");
yield* fs.makeDirectory(current, { recursive: true });
yield* fs.makeDirectory(path.dirname(manager), { recursive: true });
yield* fs.writeFileString(executablePath, "fixture");
yield* fs.writeFileString(harnessPath, "fixture");
yield* fs.writeFileString(path.join(current, "install.json"), '{"bucket":"fixture"}');
yield* fs.writeFileString(manager, "@echo off\r\n");
yield* fs.writeFileString(selectedAlias, "fixture shim");
yield* fs.writeFileString(
`${selectedAlias.slice(0, -4)}.shim`,
`path = "${executablePath}"\r\n`,
);
instancePath = [path.dirname(manager), instancePath].join(windowsHost ? ";" : ":");
selected = {
executablePath,
resolvedCommandPath: selectedAlias,
harnessPath,
source: "override",
version: null,
managedVersionDirectory: null,
};
}
const controls = { selected, failResolution: false, beforeAcquire: Effect.void };
const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = [];
const releases: Array<string | null> = [];
const launches: Array<{
Expand Down Expand Up @@ -533,4 +569,48 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
expect(h.acquisitions).toEqual([]);
}).pipe(Effect.scoped),
);

it.effect("keeps managed runtimes out of command-based maintenance", () =>
Effect.gen(function* () {
const h = yield* makeHarness();
h.controls.selected = {
...h.first,
source: "override",
resolvedCommandPath: h.first.executablePath,
};
expect((yield* h.instance.snapshot.resolveMaintenance({ fresh: true })).update).toBeNull();
}).pipe(Effect.scoped),
);

it.effect.skipIf(!windowsHost)(
"updates an external Antigravity ACP pair through its owning Scoop app",
() =>
Effect.gen(function* () {
const h = yield* makeHarness({ externalScoop: true });
const update = (yield* h.instance.snapshot.resolveMaintenance({ fresh: true })).update;
expect(update).toMatchObject({
args: ["update", "fixture/fixture-antigravity-runtime"],
windowsInstaller: { manager: "scoop", scope: "user" },
});
expect(update?.installationKey).toContain("fixture-antigravity-runtime");
expect(h.launches).toEqual([]);
}).pipe(Effect.scoped),
);

it.effect.skipIf(!windowsHost)("keeps unproven WinGet ACP paths manual-only", () =>
Effect.gen(function* () {
const h = yield* makeHarness();
const directory = h.path.join("C:\\Users\\fixture", "Microsoft", "WinGet", "Packages");
h.controls.selected = {
executablePath: h.path.join(directory, "agy_acp_server.exe"),
resolvedCommandPath: h.path.join(directory, "agy_acp_server.exe"),
harnessPath: h.path.join(directory, "localharness_external.exe"),
source: "override",
version: null,
managedVersionDirectory: null,
};
expect((yield* h.instance.snapshot.resolveMaintenance({ fresh: true })).update).toBeNull();
expect(h.launches).toEqual([]);
}).pipe(Effect.scoped),
);
});
38 changes: 37 additions & 1 deletion apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,26 @@ import {
type ProviderInstance,
} from "../ProviderDriver.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeCachedProviderMaintenanceResolution,
makeManualOnlyProviderMaintenanceCapabilities,
makePackageManagedProviderMaintenanceResolver,
} from "../providerMaintenance.ts";
import { withInstanceIdentity } from "./instanceIdentity.ts";
import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";

const DRIVER = ProviderDriverKind.make("antigravity");
const decodeSettings = Schema.decodeSync(AntigravitySettings);
const isNodeRuntimeUnavailableError = Schema.is(NodeRuntimeUnavailableError);
const MANUAL_MAINTENANCE = makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER,
packageName: null,
});
const EXTERNAL_UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER,
npmPackageName: null,
nativeUpdate: null,
});

export type AntigravityDriverEnv =
| AntigravityInstallation
Expand Down Expand Up @@ -102,7 +116,28 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
};
const authConfigIssue = antigravityAuthConfigIssue(auth);
const processEnvironment = mergeProviderInstanceEnvironment(environment);
const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);
const platform = yield* HostProcessPlatform;
const userHome = resolveAntigravityUserHome(platform, processEnvironment);
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
installation.resolve(settings.binaryPath, processEnvironment).pipe(
Effect.flatMap((executable) =>
executable.managedVersionDirectory !== null
? Effect.succeed(MANUAL_MAINTENANCE)
: EXTERNAL_UPDATE.resolve({
binaryPath: settings.binaryPath || executable.executablePath,
resolvedCommandPath: executable.resolvedCommandPath ?? executable.executablePath,
realCommandPath: executable.executablePath,
env: processEnvironment,
platform,
}),
),
Effect.orElseSucceed(() => MANUAL_MAINTENANCE),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.provideService(HostProcessPlatform, platform),
),
);
const profileDirectory = resolveAntigravityProfileDirectory(
serverConfig.stateDir,
instanceId,
Expand Down Expand Up @@ -357,6 +392,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
stampIdentity: classifyModels,
probe,
auth: { type: auth.authMethod, label: antigravityAuthLabel(auth.authMethod) },
resolveMaintenance,
supportsTextGeneration: isAntigravityTextGenerationAvailable(profileDirectory).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Expand Down
18 changes: 6 additions & 12 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ import { withInstanceIdentity } from "./instanceIdentity.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makeCachedProviderMaintenanceResolution,
makeProviderMaintenanceResolution,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
Expand All @@ -78,6 +77,7 @@ function isClaudeNativeCommandPath(commandPath: string): boolean {
const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@anthropic-ai/claude-code",
wingetPackageId: "Anthropic.ClaudeCode",
nativeUpdate: {
args: ["update"],
isCommandPath: isClaudeNativeCommandPath,
Expand Down Expand Up @@ -125,16 +125,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
enabled,
binaryPath: expandHomePath(config.binaryPath),
} satisfies ClaudeSettings;
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
),
);
const resolveMaintenance = yield* makeProviderMaintenanceResolution(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});
const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
const stampIdentity = withInstanceIdentity({
instanceId,
Expand Down
21 changes: 5 additions & 16 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ import { withInstanceIdentity } from "./instanceIdentity.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makeCachedProviderMaintenanceResolution,
makeProviderMaintenanceResolution,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
Expand Down Expand Up @@ -90,6 +89,7 @@ function makeCodexMaintenanceResolver(sharedHomePath: string) {
return makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@openai/codex",
wingetPackageId: "OpenAI.Codex",
nativeUpdate: {
args: ["update"],
isCommandPath: isCodexStandaloneCommandPath,
Expand Down Expand Up @@ -128,8 +128,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const resetCreditCoordinator = yield* CodexResetCreditCoordinator;
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
Expand Down Expand Up @@ -161,18 +159,9 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
binaryPath: expandHomePath(config.binaryPath),
homePath: homeLayout.effectiveHomePath ?? "",
} satisfies CodexSettings;
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
resolveProviderMaintenanceCapabilitiesEffect(
makeCodexMaintenanceResolver(homeLayout.sharedHomePath),
{
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
},
).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, pathService),
),
const resolveMaintenance = yield* makeProviderMaintenanceResolution(
makeCodexMaintenanceResolver(homeLayout.sharedHomePath),
{ binaryPath: effectiveConfig.binaryPath, env: processEnv },
);

// `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error
Expand Down
Loading
Loading