authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-05 20:54:49-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 20:54:49-07:00
logab67795dde448a3608d8689f7ad1975bb977e5a9
tree2d9a4c31b05c392a86d08be7cdc394326c3a14f2
parentc8872fd22aadb8d155f1bedc10576d8314f93e56
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): load user skills from ~/.gemini for every project (#10257)

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,4 +1,5 @@
1import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";1import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
2import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
2import * as Crypto from "effect/Crypto";3import * as Crypto from "effect/Crypto";
3import * as Deferred from "effect/Deferred";4import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
...@@ -49,7 +50,7 @@ import {...@@ -49,7 +50,7 @@ import {
49} from "../ProviderDriver.ts";50} from "../ProviderDriver.ts";
50import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";51import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
51import { withInstanceIdentity } from "./instanceIdentity.ts";52import { withInstanceIdentity } from "./instanceIdentity.ts";
52import { discoverAntigravitySkills } from "./AntigravitySkills.ts";53import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";
5354
54const DRIVER = ProviderDriverKind.make("antigravity");55const DRIVER = ProviderDriverKind.make("antigravity");
55const decodeSettings = Schema.decodeSync(AntigravitySettings);56const decodeSettings = Schema.decodeSync(AntigravitySettings);
...@@ -91,6 +92,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -91,6 +92,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
91 };92 };
92 const authConfigIssue = antigravityAuthConfigIssue(auth);93 const authConfigIssue = antigravityAuthConfigIssue(auth);
93 const processEnvironment = mergeProviderInstanceEnvironment(environment);94 const processEnvironment = mergeProviderInstanceEnvironment(environment);
95 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);
94 const profileDirectory = resolveAntigravityProfileDirectory(96 const profileDirectory = resolveAntigravityProfileDirectory(
95 serverConfig.stateDir,97 serverConfig.stateDir,
96 instanceId,98 instanceId,
...@@ -146,6 +148,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -146,6 +148,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
146 profileDirectory,148 profileDirectory,
147 baseEnv: processEnvironment,149 baseEnv: processEnvironment,
148 auth,150 auth,
151 userHome,
149 }).pipe(152 }).pipe(
150 Effect.provideService(FileSystem.FileSystem, fileSystem),153 Effect.provideService(FileSystem.FileSystem, fileSystem),
151 Effect.provideService(Path.Path, path),154 Effect.provideService(Path.Path, path),
...@@ -376,7 +379,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -376,7 +379,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
376 snapshotForCwd: (cwd) =>379 snapshotForCwd: (cwd) =>
377 !enabled380 !enabled
378 ? provider.snapshot.getSnapshot381 ? provider.snapshot.getSnapshot
379 : discoverAntigravitySkills({ cwd, profileDirectory }).pipe(382 : discoverAntigravitySkills({ cwd, userHome }).pipe(
380 Effect.provideService(FileSystem.FileSystem, fileSystem),383 Effect.provideService(FileSystem.FileSystem, fileSystem),
381 Effect.provideService(Path.Path, path),384 Effect.provideService(Path.Path, path),
382 Effect.flatMap((skills) => provider.snapshotForCwd(cwd, skills)),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,7 +4,7 @@ import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";5import * as Path from "effect/Path";
66
7import { discoverAntigravitySkills } from "./AntigravitySkills.ts";7import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";
8import { symlinksSupported } from "@t3tools/shared/testing/symlinks";8import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
99
10const writeSkill = Effect.fn("writeSkill")(function* (directory: string, contents: string) {10const writeSkill = Effect.fn("writeSkill")(function* (directory: string, contents: string) {
...@@ -24,20 +24,57 @@ const makeWorkspace = Effect.fn("makeWorkspace")(function* () {...@@ -24,20 +24,57 @@ const makeWorkspace = Effect.fn("makeWorkspace")(function* () {
24 });24 });
25 return {25 return {
26 cwd: path.join(temporaryDirectory, "workspace"),26 cwd: path.join(temporaryDirectory, "workspace"),
27 profileDirectory: path.join(temporaryDirectory, "profile"),27 userHome: path.join(temporaryDirectory, "home"),
28 };28 };
29});29});
3030
31it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {31it.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 it.effect("reads skill names, descriptions and paths from the current native roots", () =>69 it.effect("reads skill names, descriptions and paths from the current native roots", () =>
33 Effect.gen(function* () {70 Effect.gen(function* () {
34 const path = yield* Path.Path;71 const path = yield* Path.Path;
35 const input = yield* makeWorkspace();72 const input = yield* makeWorkspace();
36 const roots = [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 { directory: path.join(input.cwd, ".gemini", "skills"), scope: "project" },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 scope: "user",78 scope: "user",
42 },79 },
43 { directory: path.join(input.cwd, ".agents", "skills"), scope: "project" },80 { directory: path.join(input.cwd, ".agents", "skills"), scope: "project" },
...@@ -91,9 +128,9 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {...@@ -91,9 +128,9 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
91 const path = yield* Path.Path;128 const path = yield* Path.Path;
92 const input = yield* makeWorkspace();129 const input = yield* makeWorkspace();
93 const roots = [130 const roots = [
94 path.join(input.profileDirectory, "config", "skills"),131 path.join(input.userHome, ".gemini", "config", "skills"),
95 path.join(input.cwd, ".gemini", "skills"),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 path.join(input.cwd, ".agents", "skills"),134 path.join(input.cwd, ".agents", "skills"),
98 path.join(input.cwd, ".agent", "skills"),135 path.join(input.cwd, ".agent", "skills"),
99 ];136 ];
...@@ -218,7 +255,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {...@@ -218,7 +255,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
218 const input = yield* makeWorkspace();255 const input = yield* makeWorkspace();
219 const root = path.join(input.cwd, ".agents", "skills");256 const root = path.join(input.cwd, ".agents", "skills");
220 yield* writeSkill(257 yield* writeSkill(
221 path.join(input.profileDirectory, "config", "skills", "review"),258 path.join(input.userHome, ".gemini", "config", "skills", "review"),
222 "---\nname: [invalid\n---\n",259 "---\nname: [invalid\n---\n",
223 );260 );
224 const nativeOrder = [" space-copy", "!-copy", "ø-copy", "a-copy"];261 const nativeOrder = [" space-copy", "!-copy", "ø-copy", "a-copy"];
...@@ -247,7 +284,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {...@@ -247,7 +284,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
247 const fileSystem = yield* FileSystem.FileSystem;284 const fileSystem = yield* FileSystem.FileSystem;
248 const path = yield* Path.Path;285 const path = yield* Path.Path;
249 const input = yield* makeWorkspace();286 const input = yield* makeWorkspace();
250 const sourceDirectory = path.join(input.profileDirectory, "shared-review");287 const sourceDirectory = path.join(input.userHome, "shared-review");
251 yield* writeSkill(sourceDirectory, "---\nname: review\n---\n");288 yield* writeSkill(sourceDirectory, "---\nname: review\n---\n");
252 const root = path.join(input.cwd, ".agents", "skills");289 const root = path.join(input.cwd, ".agents", "skills");
253 const linkedDirectory = path.join(root, "review");290 const linkedDirectory = path.join(root, "review");
...@@ -304,3 +341,20 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {...@@ -304,3 +341,20 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
304 }),341 }),
305 );342 );
306});343});
344
345it("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,3 +1,5 @@
1import * as NodeOS from "node:os";
2
1import type { ServerProviderSkill } from "@t3tools/contracts";3import type { ServerProviderSkill } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
...@@ -7,6 +9,45 @@ import * as Schema from "effect/Schema";...@@ -7,6 +9,45 @@ import * as Schema from "effect/Schema";
7import * as Stream from "effect/Stream";9import * as Stream from "effect/Stream";
8import { parse as parseYamlDocument } from "yaml";10import { parse as parseYamlDocument } from "yaml";
911
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 */
18export 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 */
41export 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
10const MAX_SKILL_BYTES = 1_000_000;51const MAX_SKILL_BYTES = 1_000_000;
11const MAX_SCAN_BYTES = 8_000_000;52const MAX_SCAN_BYTES = 8_000_000;
12const MAX_SCAN_ENTRIES = 10_000;53const MAX_SCAN_ENTRIES = 10_000;
...@@ -118,7 +159,7 @@ const readSkill = Effect.fn("readAntigravitySkill")(function* (...@@ -118,7 +159,7 @@ const readSkill = Effect.fn("readAntigravitySkill")(function* (
118 */159 */
119export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(function* (input: {160export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(function* (input: {
120 readonly cwd: string;161 readonly cwd: string;
121 readonly profileDirectory: string;162 readonly userHome: string;
122}): Effect.fn.Return<163}): Effect.fn.Return<
123 ReadonlyArray<ServerProviderSkill>,164 ReadonlyArray<ServerProviderSkill>,
124 AntigravitySkillsProbeError,165 AntigravitySkillsProbeError,
...@@ -126,13 +167,14 @@ export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(...@@ -126,13 +167,14 @@ export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(
126> {167> {
127 const fileSystem = yield* FileSystem.FileSystem;168 const fileSystem = yield* FileSystem.FileSystem;
128 const path = yield* Path.Path;169 const path = yield* Path.Path;
170 const [configSkills, cliSkills] = antigravityUserSkillDirectories(
171 path,
172 path.join(input.userHome, ".gemini"),
173 );
129 const roots = [174 const roots = [
130 { directory: path.resolve(input.profileDirectory, "config", "skills"), scope: "user" },175 { directory: configSkills, scope: "user" },
131 { directory: path.resolve(input.cwd, ".gemini", "skills"), scope: "project" },176 { directory: path.resolve(input.cwd, ".gemini", "skills"), scope: "project" },
132 {177 { directory: cliSkills, scope: "user" },
133 directory: path.resolve(input.profileDirectory, "antigravity-cli", "skills"),
134 scope: "user",
135 },
136 { directory: path.resolve(input.cwd, ".agents", "skills"), scope: "project" },178 { directory: path.resolve(input.cwd, ".agents", "skills"), scope: "project" },
137 { directory: path.resolve(input.cwd, ".agent", "skills"), scope: "project" },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,6 +15,7 @@ import * as Ndjson from "effect/unstable/encoding/Ndjson";
15import * as ChildProcess from "effect/unstable/process/ChildProcess";15import * as ChildProcess from "effect/unstable/process/ChildProcess";
16import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";16import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
17import * as AcpErrors from "effect-acp/errors";17import * as AcpErrors from "effect-acp/errors";
18import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1819
19import {20import {
20 ANTIGRAVITY_AUTH_BROWSER_MARKER,21 ANTIGRAVITY_AUTH_BROWSER_MARKER,
...@@ -510,6 +511,42 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {...@@ -510,6 +511,42 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {
510 }),511 }),
511 );512 );
512513
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 it.effect("rewrites the GCP block on every launch and never stores the API key", () =>550 it.effect("rewrites the GCP block on every launch and never stores the API key", () =>
514 Effect.gen(function* () {551 Effect.gen(function* () {
515 const fs = yield* FileSystem.FileSystem;552 const fs = yield* FileSystem.FileSystem;
apps/server/src/provider/antigravityAuthSupport.ts+61
...@@ -1,4 +1,6 @@...@@ -1,4 +1,6 @@
1import * as NodeCrypto from "node:crypto";1import * 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.
3import * as NodeFSP from "node:fs/promises";
2// @effect-diagnostics-next-line nodeBuiltinImport:off - resolveAntigravityProfileDirectory is a pure sync helper, so it cannot use the Path service.4// @effect-diagnostics-next-line nodeBuiltinImport:off - resolveAntigravityProfileDirectory is a pure sync helper, so it cannot use the Path service.
3import * as NodePath from "node:path";5import * as NodePath from "node:path";
46
...@@ -16,6 +18,10 @@ import * as AcpErrors from "effect-acp/errors";...@@ -16,6 +18,10 @@ import * as AcpErrors from "effect-acp/errors";
1618
17import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";19import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
18import type { AcpSpawnInput } from "./acp/AcpSessionRuntime.ts";20import type { AcpSpawnInput } from "./acp/AcpSessionRuntime.ts";
21import {
22 antigravityUserSkillDirectories,
23 resolveAntigravityUserHome,
24} from "./Drivers/AntigravitySkills.ts";
1925
20export const ANTIGRAVITY_AUTH_STDOUT_PREFIX =26export const ANTIGRAVITY_AUTH_STDOUT_PREFIX =
21 "Open the following link to authenticate the ACP server: ";27 "Open the following link to authenticate the ACP server: ";
...@@ -219,6 +225,56 @@ function antigravityEnvironment(...@@ -219,6 +225,56 @@ function antigravityEnvironment(
219 };225 };
220}226}
221227
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 */
236const 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/** Prepares a private profile without reading or copying Google credentials. */278/** Prepares a private profile without reading or copying Google credentials. */
223export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(function* (input: {279export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(function* (input: {
224 readonly profileDirectory: string;280 readonly profileDirectory: string;
...@@ -226,12 +282,16 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(...@@ -226,12 +282,16 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
226 readonly runtimeExecutablePath?: string;282 readonly runtimeExecutablePath?: string;
227 readonly platform?: NodeJS.Platform;283 readonly platform?: NodeJS.Platform;
228 readonly auth?: AntigravityAuthConfig;284 readonly auth?: AntigravityAuthConfig;
285 /** Home the agent expands `~` against. Defaults to the launch environment's. */
286 readonly userHome?: string;
229}) {287}) {
230 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;288 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;
231 const fs = yield* FileSystem.FileSystem;289 const fs = yield* FileSystem.FileSystem;
232 const path = yield* Path.Path;290 const path = yield* Path.Path;
233 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;291 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
234 const platform = input.platform ?? (yield* HostProcessPlatform);292 const platform = input.platform ?? (yield* HostProcessPlatform);
293 const userHome =
294 input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env);
235 const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* HostProcessExecutablePath);295 const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* HostProcessExecutablePath);
236 const helperExecutable =296 const helperExecutable =
237 platform === "win32" ? runtimeExecutablePath.replaceAll("\\", "/") : runtimeExecutablePath;297 platform === "win32" ? runtimeExecutablePath.replaceAll("\\", "/") : runtimeExecutablePath;
...@@ -326,6 +386,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(...@@ -326,6 +386,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
326 authSupportError("The Antigravity profile settings could not be written."),386 authSupportError("The Antigravity profile settings could not be written."),
327 ),387 ),
328 );388 );
389 yield* linkAntigravityUserSkills({ profileDirectory: geminiHome, userHome, platform });
329 return profile;390 return profile;
330});391});
331392
docs/internals/providers.md+3-1
...@@ -25,7 +25,9 @@ See the [adapter](../../apps/server/src/provider/Layers/OpenCodeAdapter.ts)....@@ -25,7 +25,9 @@ See the [adapter](../../apps/server/src/provider/Layers/OpenCodeAdapter.ts).
25Antigravity separates account profiles per instance while sharing installed executables across the25Antigravity separates account profiles per instance while sharing installed executables across the
26environment. It forces file-based credential storage because the native macOS keychain entry would26environment. It forces file-based credential storage because the native macOS keychain entry would
27otherwise be shared across instances. The launch environment removes ambient Google credentials,27otherwise be shared across instances. The launch environment removes ambient Google credentials,
28so an instance cannot silently use another account or billing project.28so an instance cannot silently use another account or billing project. The agent also resolves
29its user-global skill directories under that profile, so the profile links those two directories
30back to the user's real `~/.gemini`; MCP servers, hooks, and rules there stay out of the profile.
29See [profile isolation](../../apps/server/src/provider/antigravityAuthSupport.ts).31See [profile isolation](../../apps/server/src/provider/antigravityAuthSupport.ts).
3032
31The [Antigravity installer](../../apps/server/src/provider/AntigravityInstallation.ts) outlives33The [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,6 +88,10 @@ legacy `.agent/skills` directory. Among these project locations, the first copy
88wins in this order: `.gemini/skills`, `.agents/skills`, `.agent/skills`. See88wins in this order: `.gemini/skills`, `.agents/skills`, `.agent/skills`. See
89[commands and skills](./composer.md#commands-and-skills) for invoking them.89[commands and skills](./composer.md#commands-and-skills) for invoking them.
9090
91Skills for every project go in `~/.gemini/config/skills` or
92`~/.gemini/antigravity-cli/skills`. Antigravity does not read `~/.agents/skills`,
93so a skill there only appears when the project itself is your home directory.
94
91Antigravity accepts images, PDFs, text files, and supported audio formats directly.95Antigravity accepts images, PDFs, text files, and supported audio formats directly.
92Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and96Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and
9350 MiB total attachments per message. Unsupported formats are rejected. These9750 MiB total attachments per message. Unsupported formats are rejected. These