| author | |
| committer | |
| log | ab67795dde448a3608d8689f7ad1975bb977e5a9 |
| tree | 2d9a4c31b05c392a86d08be7cdc394326c3a14f2 |
| parent | c8872fd22aadb8d155f1bedc10576d8314f93e56 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>7 files changed, 220 insertions(+), 17 deletions(-)
apps/server/src/provider/Drivers/AntigravityDriver.ts+5-2| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 2 | 3 | import * as Crypto from "effect/Crypto"; |
| 3 | 4 | import * as Deferred from "effect/Deferred"; |
| 4 | 5 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -49,7 +50,7 @@ import { |
| 49 | 50 | } from "../ProviderDriver.ts"; |
| 50 | 51 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 51 | 52 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 52 | import { discoverAntigravitySkills } from "./AntigravitySkills.ts"; | |
| 53 | import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts"; | |
| 53 | 54 | |
| 54 | 55 | const DRIVER = ProviderDriverKind.make("antigravity"); |
| 55 | 56 | const decodeSettings = Schema.decodeSync(AntigravitySettings); |
| ... | ... | @@ -91,6 +92,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 91 | 92 | }; |
| 92 | 93 | const authConfigIssue = antigravityAuthConfigIssue(auth); |
| 93 | 94 | const processEnvironment = mergeProviderInstanceEnvironment(environment); |
| 95 | const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment); | |
| 94 | 96 | const profileDirectory = resolveAntigravityProfileDirectory( |
| 95 | 97 | serverConfig.stateDir, |
| 96 | 98 | instanceId, |
| ... | ... | @@ -146,6 +148,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 146 | 148 | profileDirectory, |
| 147 | 149 | baseEnv: processEnvironment, |
| 148 | 150 | auth, |
| 151 | userHome, | |
| 149 | 152 | }).pipe( |
| 150 | 153 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 151 | 154 | Effect.provideService(Path.Path, path), |
| ... | ... | @@ -376,7 +379,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 376 | 379 | snapshotForCwd: (cwd) => |
| 377 | 380 | !enabled |
| 378 | 381 | ? provider.snapshot.getSnapshot |
| 379 | : discoverAntigravitySkills({ cwd, profileDirectory }).pipe( | |
| 382 | : discoverAntigravitySkills({ cwd, userHome }).pipe( | |
| 380 | 383 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 381 | 384 | Effect.provideService(Path.Path, path), |
| 382 | 385 | Effect.flatMap((skills) => provider.snapshotForCwd(cwd, skills)), |
apps/server/src/provider/Drivers/AntigravitySkills.test.ts+62-8| ... | ... | @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | 5 | import * as Path from "effect/Path"; |
| 6 | 6 | |
| 7 | import { discoverAntigravitySkills } from "./AntigravitySkills.ts"; | |
| 7 | import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts"; | |
| 8 | 8 | import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; |
| 9 | 9 | |
| 10 | 10 | const writeSkill = Effect.fn("writeSkill")(function* (directory: string, contents: string) { |
| ... | ... | @@ -24,20 +24,57 @@ const makeWorkspace = Effect.fn("makeWorkspace")(function* () { |
| 24 | 24 | }); |
| 25 | 25 | return { |
| 26 | 26 | cwd: path.join(temporaryDirectory, "workspace"), |
| 27 | profileDirectory: path.join(temporaryDirectory, "profile"), | |
| 27 | userHome: path.join(temporaryDirectory, "home"), | |
| 28 | 28 | }; |
| 29 | 29 | }); |
| 30 | 30 | |
| 31 | 31 | it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { |
| 32 | it.effect("does not read user skills from a nested project or from ~/.agents", () => | |
| 33 | Effect.gen(function* () { | |
| 34 | const path = yield* Path.Path; | |
| 35 | const input = yield* makeWorkspace(); | |
| 36 | const nested = { ...input, cwd: path.join(input.userHome, "AI", "Projects", "Something") }; | |
| 37 | const skillPath = yield* writeSkill( | |
| 38 | path.join(input.userHome, ".gemini", "config", "skills", "review"), | |
| 39 | "---\nname: review\ndescription: Review changes.\n---\n", | |
| 40 | ); | |
| 41 | yield* writeSkill( | |
| 42 | path.join(input.userHome, ".agents", "skills", "ignored"), | |
| 43 | "---\nname: ignored\n---\n", | |
| 44 | ); | |
| 45 | ||
| 46 | assert.deepEqual(yield* discoverAntigravitySkills(nested), [ | |
| 47 | { | |
| 48 | name: "review", | |
| 49 | description: "Review changes.", | |
| 50 | path: skillPath, | |
| 51 | scope: "user", | |
| 52 | enabled: true, | |
| 53 | }, | |
| 54 | ]); | |
| 55 | // A project rooted at the home directory sees ~/.agents/skills as its own. | |
| 56 | assert.deepEqual( | |
| 57 | (yield* discoverAntigravitySkills({ ...input, cwd: input.userHome })).map((skill) => [ | |
| 58 | skill.name, | |
| 59 | skill.scope, | |
| 60 | ]), | |
| 61 | [ | |
| 62 | ["ignored", "project"], | |
| 63 | ["review", "user"], | |
| 64 | ], | |
| 65 | ); | |
| 66 | }), | |
| 67 | ); | |
| 68 | ||
| 32 | 69 | it.effect("reads skill names, descriptions and paths from the current native roots", () => |
| 33 | 70 | Effect.gen(function* () { |
| 34 | 71 | const path = yield* Path.Path; |
| 35 | 72 | const input = yield* makeWorkspace(); |
| 36 | 73 | const roots = [ |
| 37 | { directory: path.join(input.profileDirectory, "config", "skills"), scope: "user" }, | |
| 74 | { directory: path.join(input.userHome, ".gemini", "config", "skills"), scope: "user" }, | |
| 38 | 75 | { directory: path.join(input.cwd, ".gemini", "skills"), scope: "project" }, |
| 39 | 76 | { |
| 40 | directory: path.join(input.profileDirectory, "antigravity-cli", "skills"), | |
| 77 | directory: path.join(input.userHome, ".gemini", "antigravity-cli", "skills"), | |
| 41 | 78 | scope: "user", |
| 42 | 79 | }, |
| 43 | 80 | { directory: path.join(input.cwd, ".agents", "skills"), scope: "project" }, |
| ... | ... | @@ -91,9 +128,9 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { |
| 91 | 128 | const path = yield* Path.Path; |
| 92 | 129 | const input = yield* makeWorkspace(); |
| 93 | 130 | const roots = [ |
| 94 | path.join(input.profileDirectory, "config", "skills"), | |
| 131 | path.join(input.userHome, ".gemini", "config", "skills"), | |
| 95 | 132 | path.join(input.cwd, ".gemini", "skills"), |
| 96 | path.join(input.profileDirectory, "antigravity-cli", "skills"), | |
| 133 | path.join(input.userHome, ".gemini", "antigravity-cli", "skills"), | |
| 97 | 134 | path.join(input.cwd, ".agents", "skills"), |
| 98 | 135 | path.join(input.cwd, ".agent", "skills"), |
| 99 | 136 | ]; |
| ... | ... | @@ -218,7 +255,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { |
| 218 | 255 | const input = yield* makeWorkspace(); |
| 219 | 256 | const root = path.join(input.cwd, ".agents", "skills"); |
| 220 | 257 | yield* writeSkill( |
| 221 | path.join(input.profileDirectory, "config", "skills", "review"), | |
| 258 | path.join(input.userHome, ".gemini", "config", "skills", "review"), | |
| 222 | 259 | "---\nname: [invalid\n---\n", |
| 223 | 260 | ); |
| 224 | 261 | const nativeOrder = [" space-copy", "!-copy", "ø-copy", "a-copy"]; |
| ... | ... | @@ -247,7 +284,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { |
| 247 | 284 | const fileSystem = yield* FileSystem.FileSystem; |
| 248 | 285 | const path = yield* Path.Path; |
| 249 | 286 | const input = yield* makeWorkspace(); |
| 250 | const sourceDirectory = path.join(input.profileDirectory, "shared-review"); | |
| 287 | const sourceDirectory = path.join(input.userHome, "shared-review"); | |
| 251 | 288 | yield* writeSkill(sourceDirectory, "---\nname: review\n---\n"); |
| 252 | 289 | const root = path.join(input.cwd, ".agents", "skills"); |
| 253 | 290 | const linkedDirectory = path.join(root, "review"); |
| ... | ... | @@ -304,3 +341,20 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { |
| 304 | 341 | }), |
| 305 | 342 | ); |
| 306 | 343 | }); |
| 344 | ||
| 345 | it("resolves the home the agent expands ~ against", () => { | |
| 346 | assert.equal( | |
| 347 | resolveAntigravityUserHome("linux", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), | |
| 348 | "/home/user", | |
| 349 | ); | |
| 350 | assert.equal( | |
| 351 | resolveAntigravityUserHome("win32", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), | |
| 352 | "C:\\Users\\user", | |
| 353 | ); | |
| 354 | assert.equal( | |
| 355 | resolveAntigravityUserHome("win32", { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }), | |
| 356 | "D:\\Users\\alice", | |
| 357 | ); | |
| 358 | assert.equal(resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }), "/Users/a b "); | |
| 359 | assert.equal(resolveAntigravityUserHome("darwin", { HOME: "" }).length > 0, true); | |
| 360 | }); |
apps/server/src/provider/Drivers/AntigravitySkills.ts+48-6| ... | ... | @@ -1,3 +1,5 @@ |
| 1 | import * as NodeOS from "node:os"; | |
| 2 | ||
| 1 | 3 | import type { ServerProviderSkill } from "@t3tools/contracts"; |
| 2 | 4 | import * as Effect from "effect/Effect"; |
| 3 | 5 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -7,6 +9,45 @@ import * as Schema from "effect/Schema"; |
| 7 | 9 | import * as Stream from "effect/Stream"; |
| 8 | 10 | import { parse as parseYamlDocument } from "yaml"; |
| 9 | 11 | |
| 12 | /** | |
| 13 | * The home directory the agent expands `~` against, matching Python's | |
| 14 | * `os.path.expanduser` in the launch environment T3 hands the process: | |
| 15 | * `USERPROFILE`, then `HOMEDRIVE` + `HOMEPATH`, on Windows and `HOME` | |
| 16 | * elsewhere. Values are used verbatim; a path may contain spaces. | |
| 17 | */ | |
| 18 | export function resolveAntigravityUserHome( | |
| 19 | platform: NodeJS.Platform, | |
| 20 | environment: NodeJS.ProcessEnv, | |
| 21 | ): string { | |
| 22 | if (platform === "win32") { | |
| 23 | if (environment.USERPROFILE) return environment.USERPROFILE; | |
| 24 | if (environment.HOMEDRIVE && environment.HOMEPATH) { | |
| 25 | return `${environment.HOMEDRIVE}${environment.HOMEPATH}`; | |
| 26 | } | |
| 27 | return NodeOS.homedir(); | |
| 28 | } | |
| 29 | return environment.HOME || NodeOS.homedir(); | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * The agent's two user-global skill directories under a Gemini home, in | |
| 34 | * native precedence order: `config/skills` is shared with the Antigravity IDE | |
| 35 | * and CLI, and `antigravity-cli/skills` is where the `agy` CLI installs | |
| 36 | * skills. The agent resolves both under `GEMINI_HOME`, which T3 points at a | |
| 37 | * private profile, so the profile links these back to the user's `~/.gemini`. | |
| 38 | * `~/.agents/skills` is not read: the agent only treats `.agents/skills` as a | |
| 39 | * project directory. | |
| 40 | */ | |
| 41 | export function antigravityUserSkillDirectories( | |
| 42 | path: Path.Path, | |
| 43 | geminiHome: string, | |
| 44 | ): readonly [configSkills: string, cliSkills: string] { | |
| 45 | return [ | |
| 46 | path.join(geminiHome, "config", "skills"), | |
| 47 | path.join(geminiHome, "antigravity-cli", "skills"), | |
| 48 | ]; | |
| 49 | } | |
| 50 | ||
| 10 | 51 | const MAX_SKILL_BYTES = 1_000_000; |
| 11 | 52 | const MAX_SCAN_BYTES = 8_000_000; |
| 12 | 53 | const MAX_SCAN_ENTRIES = 10_000; |
| ... | ... | @@ -118,7 +159,7 @@ const readSkill = Effect.fn("readAntigravitySkill")(function* ( |
| 118 | 159 | */ |
| 119 | 160 | export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(function* (input: { |
| 120 | 161 | readonly cwd: string; |
| 121 | readonly profileDirectory: string; | |
| 162 | readonly userHome: string; | |
| 122 | 163 | }): Effect.fn.Return< |
| 123 | 164 | ReadonlyArray<ServerProviderSkill>, |
| 124 | 165 | AntigravitySkillsProbeError, |
| ... | ... | @@ -126,13 +167,14 @@ export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")( |
| 126 | 167 | > { |
| 127 | 168 | const fileSystem = yield* FileSystem.FileSystem; |
| 128 | 169 | const path = yield* Path.Path; |
| 170 | const [configSkills, cliSkills] = antigravityUserSkillDirectories( | |
| 171 | path, | |
| 172 | path.join(input.userHome, ".gemini"), | |
| 173 | ); | |
| 129 | 174 | const roots = [ |
| 130 | { directory: path.resolve(input.profileDirectory, "config", "skills"), scope: "user" }, | |
| 175 | { directory: configSkills, scope: "user" }, | |
| 131 | 176 | { directory: path.resolve(input.cwd, ".gemini", "skills"), scope: "project" }, |
| 132 | { | |
| 133 | directory: path.resolve(input.profileDirectory, "antigravity-cli", "skills"), | |
| 134 | scope: "user", | |
| 135 | }, | |
| 177 | { directory: cliSkills, scope: "user" }, | |
| 136 | 178 | { directory: path.resolve(input.cwd, ".agents", "skills"), scope: "project" }, |
| 137 | 179 | { directory: path.resolve(input.cwd, ".agent", "skills"), scope: "project" }, |
| 138 | 180 | ]; |
apps/server/src/provider/antigravityAuthSupport.test.ts+37| ... | ... | @@ -15,6 +15,7 @@ import * as Ndjson from "effect/unstable/encoding/Ndjson"; |
| 15 | 15 | import * as ChildProcess from "effect/unstable/process/ChildProcess"; |
| 16 | 16 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 17 | 17 | import * as AcpErrors from "effect-acp/errors"; |
| 18 | import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; | |
| 18 | 19 | |
| 19 | 20 | import { |
| 20 | 21 | ANTIGRAVITY_AUTH_BROWSER_MARKER, |
| ... | ... | @@ -510,6 +511,42 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => { |
| 510 | 511 | }), |
| 511 | 512 | ); |
| 512 | 513 | |
| 514 | it.effect.skipIf(!symlinksSupported)( | |
| 515 | "links the user's global skill directories into the profile without touching real content", | |
| 516 | () => | |
| 517 | Effect.gen(function* () { | |
| 518 | const fs = yield* FileSystem.FileSystem; | |
| 519 | const path = yield* Path.Path; | |
| 520 | const temporaryDirectory = yield* fs.makeTempDirectoryScoped(); | |
| 521 | const userHome = path.join(temporaryDirectory, "home"); | |
| 522 | const profileDirectory = path.join(temporaryDirectory, "profile"); | |
| 523 | const configSkills = path.join(userHome, ".gemini", "config", "skills"); | |
| 524 | const cliSkills = path.join(userHome, ".gemini", "antigravity-cli", "skills"); | |
| 525 | yield* fs.makeDirectory(path.join(configSkills, "review"), { recursive: true }); | |
| 526 | ||
| 527 | yield* prepareAntigravityProfile({ profileDirectory, userHome }); | |
| 528 | const configLink = path.join(profileDirectory, "config", "skills"); | |
| 529 | const cliLink = path.join(profileDirectory, "antigravity-cli", "skills"); | |
| 530 | expect(yield* fs.readLink(configLink)).toBe(configSkills); | |
| 531 | expect(yield* fs.readLink(cliLink)).toBe(cliSkills); | |
| 532 | expect(yield* fs.exists(path.join(configLink, "review"))).toBe(true); | |
| 533 | // Only the skill directories are shared; the rest of the profile stays private. | |
| 534 | expect(yield* fs.exists(path.join(profileDirectory, "config", "mcp_config.json"))).toBe( | |
| 535 | false, | |
| 536 | ); | |
| 537 | ||
| 538 | // A stale link is repointed; a real directory the user placed there is kept. | |
| 539 | yield* fs.remove(cliLink); | |
| 540 | yield* fs.symlink(path.join(temporaryDirectory, "elsewhere"), cliLink); | |
| 541 | yield* fs.remove(configLink); | |
| 542 | yield* fs.makeDirectory(path.join(configLink, "own-skill"), { recursive: true }); | |
| 543 | yield* prepareAntigravityProfile({ profileDirectory, userHome }); | |
| 544 | expect(yield* fs.readLink(cliLink)).toBe(cliSkills); | |
| 545 | expect(yield* fs.exists(path.join(configLink, "own-skill"))).toBe(true); | |
| 546 | expect((yield* fs.stat(configLink)).type).toBe("Directory"); | |
| 547 | }), | |
| 548 | ); | |
| 549 | ||
| 513 | 550 | it.effect("rewrites the GCP block on every launch and never stores the API key", () => |
| 514 | 551 | Effect.gen(function* () { |
| 515 | 552 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/provider/antigravityAuthSupport.ts+61| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | 1 | import * as NodeCrypto from "node:crypto"; |
| 2 | // @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation. | |
| 3 | import * as NodeFSP from "node:fs/promises"; | |
| 2 | 4 | // @effect-diagnostics-next-line nodeBuiltinImport:off - resolveAntigravityProfileDirectory is a pure sync helper, so it cannot use the Path service. |
| 3 | 5 | import * as NodePath from "node:path"; |
| 4 | 6 | |
| ... | ... | @@ -16,6 +18,10 @@ import * as AcpErrors from "effect-acp/errors"; |
| 16 | 18 | |
| 17 | 19 | import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; |
| 18 | 20 | import type { AcpSpawnInput } from "./acp/AcpSessionRuntime.ts"; |
| 21 | import { | |
| 22 | antigravityUserSkillDirectories, | |
| 23 | resolveAntigravityUserHome, | |
| 24 | } from "./Drivers/AntigravitySkills.ts"; | |
| 19 | 25 | |
| 20 | 26 | export const ANTIGRAVITY_AUTH_STDOUT_PREFIX = |
| 21 | 27 | "Open the following link to authenticate the ACP server: "; |
| ... | ... | @@ -219,6 +225,56 @@ function antigravityEnvironment( |
| 219 | 225 | }; |
| 220 | 226 | } |
| 221 | 227 | |
| 228 | /** | |
| 229 | * The agent reads its user-global skills under `GEMINI_HOME`, which T3 points | |
| 230 | * at the private profile. Link the two skill directories back to the user's | |
| 231 | * real `~/.gemini` so global skills load, while MCP servers, hooks, and | |
| 232 | * credentials stay isolated. Best effort: a link that cannot be made only | |
| 233 | * costs global skills, never the session. A real directory at the link path | |
| 234 | * is the user's own content and is left alone. | |
| 235 | */ | |
| 236 | const linkAntigravityUserSkills = Effect.fn("linkAntigravityUserSkills")(function* (input: { | |
| 237 | readonly profileDirectory: string; | |
| 238 | readonly userHome: string; | |
| 239 | readonly platform: NodeJS.Platform; | |
| 240 | }): Effect.fn.Return<void, never, FileSystem.FileSystem | Path.Path> { | |
| 241 | const fs = yield* FileSystem.FileSystem; | |
| 242 | const path = yield* Path.Path; | |
| 243 | const links = antigravityUserSkillDirectories(path, input.profileDirectory); | |
| 244 | const targets = antigravityUserSkillDirectories(path, path.join(input.userHome, ".gemini")); | |
| 245 | for (const [link, target] of [ | |
| 246 | [links[0], targets[0]], | |
| 247 | [links[1], targets[1]], | |
| 248 | ] as const) { | |
| 249 | yield* Effect.gen(function* () { | |
| 250 | const existing = yield* fs.readLink(link).pipe( | |
| 251 | Effect.map((value): string | undefined => path.resolve(path.dirname(link), value)), | |
| 252 | Effect.catch((error) => | |
| 253 | error.reason._tag === "NotFound" ? Effect.succeed(undefined) : Effect.fail(error), | |
| 254 | ), | |
| 255 | ); | |
| 256 | if (existing === target) return; | |
| 257 | if (existing !== undefined) { | |
| 258 | yield* fs.remove(link); | |
| 259 | } | |
| 260 | yield* fs.makeDirectory(path.dirname(link), { recursive: true }); | |
| 261 | yield* Effect.tryPromise(() => | |
| 262 | NodeFSP.symlink(target, link, input.platform === "win32" ? "junction" : "dir"), | |
| 263 | ); | |
| 264 | }).pipe( | |
| 265 | // A non-symlink at the link path fails `readLink`; anything else is a | |
| 266 | // filesystem refusal. Both leave the profile usable. | |
| 267 | Effect.catch((error) => | |
| 268 | Effect.logWarning("Antigravity user skills are not linked into the profile.", { | |
| 269 | link, | |
| 270 | target, | |
| 271 | error, | |
| 272 | }), | |
| 273 | ), | |
| 274 | ); | |
| 275 | } | |
| 276 | }); | |
| 277 | ||
| 222 | 278 | /** Prepares a private profile without reading or copying Google credentials. */ |
| 223 | 279 | export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(function* (input: { |
| 224 | 280 | readonly profileDirectory: string; |
| ... | ... | @@ -226,12 +282,16 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( |
| 226 | 282 | readonly runtimeExecutablePath?: string; |
| 227 | 283 | readonly platform?: NodeJS.Platform; |
| 228 | 284 | readonly auth?: AntigravityAuthConfig; |
| 285 | /** Home the agent expands `~` against. Defaults to the launch environment's. */ | |
| 286 | readonly userHome?: string; | |
| 229 | 287 | }) { |
| 230 | 288 | const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH; |
| 231 | 289 | const fs = yield* FileSystem.FileSystem; |
| 232 | 290 | const path = yield* Path.Path; |
| 233 | 291 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 234 | 292 | const platform = input.platform ?? (yield* HostProcessPlatform); |
| 293 | const userHome = | |
| 294 | input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env); | |
| 235 | 295 | const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* HostProcessExecutablePath); |
| 236 | 296 | const helperExecutable = |
| 237 | 297 | platform === "win32" ? runtimeExecutablePath.replaceAll("\\", "/") : runtimeExecutablePath; |
| ... | ... | @@ -326,6 +386,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( |
| 326 | 386 | authSupportError("The Antigravity profile settings could not be written."), |
| 327 | 387 | ), |
| 328 | 388 | ); |
| 389 | yield* linkAntigravityUserSkills({ profileDirectory: geminiHome, userHome, platform }); | |
| 329 | 390 | return profile; |
| 330 | 391 | }); |
| 331 | 392 |
docs/internals/providers.md+3-1| ... | ... | @@ -25,7 +25,9 @@ See the [adapter](../../apps/server/src/provider/Layers/OpenCodeAdapter.ts). |
| 25 | 25 | Antigravity separates account profiles per instance while sharing installed executables across the |
| 26 | 26 | environment. It forces file-based credential storage because the native macOS keychain entry would |
| 27 | 27 | otherwise be shared across instances. The launch environment removes ambient Google credentials, |
| 28 | so an instance cannot silently use another account or billing project. | |
| 28 | so an instance cannot silently use another account or billing project. The agent also resolves | |
| 29 | its user-global skill directories under that profile, so the profile links those two directories | |
| 30 | back to the user's real `~/.gemini`; MCP servers, hooks, and rules there stay out of the profile. | |
| 29 | 31 | See [profile isolation](../../apps/server/src/provider/antigravityAuthSupport.ts). |
| 30 | 32 | |
| 31 | 33 | The [Antigravity installer](../../apps/server/src/provider/AntigravityInstallation.ts) outlives |
docs/user/providers-antigravity.md+4| ... | ... | @@ -88,6 +88,10 @@ legacy `.agent/skills` directory. Among these project locations, the first copy |
| 88 | 88 | wins in this order: `.gemini/skills`, `.agents/skills`, `.agent/skills`. See |
| 89 | 89 | [commands and skills](./composer.md#commands-and-skills) for invoking them. |
| 90 | 90 | |
| 91 | Skills for every project go in `~/.gemini/config/skills` or | |
| 92 | `~/.gemini/antigravity-cli/skills`. Antigravity does not read `~/.agents/skills`, | |
| 93 | so a skill there only appears when the project itself is your home directory. | |
| 94 | ||
| 91 | 95 | Antigravity accepts images, PDFs, text files, and supported audio formats directly. |
| 92 | 96 | Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and |
| 93 | 97 | 50 MiB total attachments per message. Unsupported formats are rejected. These |