From fe20210cf87ae833ce52695e089ff4e49fa7a9f5 Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:54:21 +0300 Subject: [PATCH 1/6] fix: load Antigravity plugin skills Antigravity ACP reads only standalone skill roots, so installed plugin skills need discovery and per-session registration. --- .../src/provider/Drivers/AntigravityDriver.ts | 23 ++- .../Drivers/AntigravitySkills.test.ts | 64 ++++++- .../src/provider/Drivers/AntigravitySkills.ts | 178 +++++++++++++++--- 3 files changed, 237 insertions(+), 28 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index a8f97b0f14743..34f0ea3b6b907 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -66,7 +66,11 @@ import { } from "@t3tools/provider-core/server/driver"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; -import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts"; +import { + discoverAntigravitySkills, + prepareAntigravityPluginSkills, + resolveAntigravityUserHome, +} from "./AntigravitySkills.ts"; const DRIVER = ProviderDriverKind.make("antigravity"); const decodeSettings = Schema.decodeSync(AntigravitySettings); @@ -245,8 +249,25 @@ export const AntigravityDriver: ProviderDriver< ), ), ); + const pluginDirectories = yield* prepareAntigravityPluginSkills({ + userHome, + runtimeTempDirectory, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + Effect.mapError( + (cause) => + new ProviderSetupError({ + instanceId, + operation: "start", + detail: "Could not prepare Antigravity plugin skills.", + cause, + }), + ), + ); const runtime = yield* makeAntigravityAcpRuntime({ ...input, + additionalDirectories: [...(input.additionalDirectories ?? []), ...pluginDirectories], runtimeVersion: executable.version, authMethod: auth.authMethod, childProcessSpawner: spawner, diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 5060d8a24e5ed..71dd2d34b52d3 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; -import { discoverAntigravitySkills } from "./AntigravitySkills.ts"; +import { discoverAntigravitySkills, prepareAntigravityPluginSkills } from "./AntigravitySkills.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const writeSkill = Effect.fn("writeSkill")(function* (directory: string, contents: string) { @@ -93,6 +93,68 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { }), ); + it.effect( + "reads enabled plugin skills without merging same-name skills from different plugins", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const input = yield* makeWorkspace(); + const config = path.join(input.userHome, ".gemini", "config"); + const expected = []; + for (const name of ["alpha", "beta", "off", "disabled", "unregistered"]) { + const plugin = path.join(config, "plugins", name); + const skillPath = yield* writeSkill( + path.join(plugin, "skills", "review"), + "---\nname: review\ndescription: Review changes.\n---\n", + ); + if (name !== "unregistered") { + yield* fileSystem.writeFileString( + path.join(plugin, "plugin.json"), + JSON.stringify({ + name: `Display ${name}`, + disabled: name === "beta" || name === "disabled", + }), + ); + } + if (name === "alpha" || name === "beta") { + expected.push({ + name: `${name}:review`, + description: "Review changes.", + path: skillPath, + scope: "user", + enabled: true, + }); + } + } + yield* fileSystem.writeFileString( + path.join(config, "config.json"), + JSON.stringify({ plugins: { beta: { enabled: true }, off: { enabled: false } } }), + ); + assert.deepEqual(yield* discoverAntigravitySkills(input), expected); + const directories = yield* prepareAntigravityPluginSkills({ + userHome: input.userHome, + runtimeTempDirectory: yield* fileSystem.makeTempDirectoryScoped(), + }); + const nativeSkills = yield* discoverAntigravitySkills({ + cwd: directories[0]!, + userHome: path.join(input.userHome, "empty"), + }); + assert.deepEqual( + nativeSkills.map((skill) => skill.name), + ["alpha:review", "beta:review"], + ); + for (const [index, skill] of nativeSkills.entries()) { + assert.include(yield* fileSystem.readFileString(skill.path), expected[index]!.path); + assert.equal( + directories[index + 1], + path.dirname(path.dirname(path.dirname(expected[index]!.path))), + ); + assert.include(yield* fileSystem.readFileString(expected[index]!.path), "name: review\n"); + } + }), + ); + it.effect("uses native root order for duplicate names", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index 19e9d8d293f48..6c47321f5e717 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -47,7 +47,7 @@ const decodeSkillFrontmatter = Schema.decodeUnknownSync(SkillFrontmatter); export class AntigravitySkillsProbeError extends Schema.TaggedError()( "AntigravitySkillsProbeError", { - reason: Schema.Literals(["scan-budget-exhausted", "filesystem-error"]), + reason: Schema.Literals(["scan-budget-exhausted", "filesystem-error", "invalid-config"]), path: Schema.String, cause: Schema.optional(Schema.Defect()), }, @@ -55,7 +55,9 @@ export class AntigravitySkillsProbeError extends Schema.TaggedError, - AntigravitySkillsProbeError, - FileSystem.FileSystem | Path.Path -> { +interface SkillRoot { + readonly directory: string; + readonly scope: string; +} + +const scanSkillDirectories = Effect.fn("scanAntigravitySkillDirectories")(function* ( + roots: ReadonlyArray, + budget: ScanBudget, +) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const [configSkills, cliSkills] = antigravityUserSkillDirectories( - path, - path.join(input.userHome, ".gemini"), - ); - const roots = [ - { directory: configSkills, scope: "user" }, - { directory: path.resolve(input.cwd, ".gemini", "skills"), scope: "project" }, - { directory: cliSkills, scope: "user" }, - { directory: path.resolve(input.cwd, ".agents", "skills"), scope: "project" }, - { directory: path.resolve(input.cwd, ".agent", "skills"), scope: "project" }, - ]; - const budget: ScanBudget = { - remainingBytes: MAX_SCAN_BYTES, - remainingEntries: MAX_SCAN_ENTRIES, - }; const skillsByName = new Map(); const scanDirectory = Effect.fn("scanAntigravitySkillDirectory")(function* ( @@ -216,3 +203,142 @@ export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")( } return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name)); }); + +const PluginManifest = Schema.fromJsonString( + Schema.Struct({ disabled: Schema.optional(Schema.Boolean) }), +); +const PluginPreferences = Schema.fromJsonString( + Schema.Struct({ + plugins: Schema.optional( + Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.optional(Schema.Boolean) })), + ), + }), +); + +const decodePluginManifest = Schema.decodeEffect(PluginManifest); +const decodePluginPreferences = Schema.decodeEffect(PluginPreferences); +const encodeString = Schema.encodeSync(Schema.fromJsonString(Schema.String)); + +const discoverAntigravityPlugins = Effect.fn("discoverAntigravityPlugins")(function* ( + userHome: string, +) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const config = path.join(userHome, ".gemini", "config"); + const directory = path.join(config, "plugins"); + const entries = yield* readIfPresent(fileSystem.readDirectory(directory), directory); + if (entries === undefined) return []; + const budget: ScanBudget = { + remainingBytes: MAX_SCAN_BYTES, + remainingEntries: MAX_SCAN_ENTRIES, + }; + if (entries.length > budget.remainingEntries) { + return yield* new AntigravitySkillsProbeError({ + reason: "scan-budget-exhausted", + path: directory, + }); + } + budget.remainingEntries -= entries.length; + const preferencesPath = path.join(config, "config.json"); + const preferencesContents = yield* readSkill(preferencesPath, budget); + const preferences = + preferencesContents === undefined + ? { plugins: {} } + : yield* decodePluginPreferences(preferencesContents).pipe( + Effect.mapError( + (cause) => + new AntigravitySkillsProbeError({ + reason: "invalid-config", + path: preferencesPath, + cause, + }), + ), + ); + const plugins = []; + for (const entry of entries.toSorted()) { + const pluginDirectory = path.join(directory, entry); + if ( + (yield* readIfPresent(fileSystem.stat(pluginDirectory), pluginDirectory))?.type !== + "Directory" + ) + continue; + const manifestPath = path.join(pluginDirectory, "plugin.json"); + const contents = yield* readSkill(manifestPath, budget); + if (contents === undefined) continue; + const manifest = yield* decodePluginManifest(contents).pipe( + Effect.mapError( + (cause) => + new AntigravitySkillsProbeError({ reason: "invalid-config", path: manifestPath, cause }), + ), + ); + if (!(preferences.plugins?.[entry]?.enabled ?? !manifest.disabled)) continue; + const skills = yield* scanSkillDirectories( + [{ directory: path.join(pluginDirectory, "skills"), scope: "user" }], + budget, + ); + if (skills.length === 0) continue; + const namespace = /^[a-zA-Z0-9]/.test(entry) ? entry : `plugin${entry}`; + plugins.push({ + directory: pluginDirectory, + skills: skills.map((skill) => ({ ...skill, name: `${namespace}:${skill.name}` })), + }); + } + return plugins; +}); + +/** Keep read errors typed so workspace snapshots cannot cache partial results. */ +export const discoverAntigravitySkills = Effect.fn("discoverAntigravitySkills")(function* (input: { + readonly cwd: string; + readonly userHome: string; +}): Effect.fn.Return< + ReadonlyArray, + AntigravitySkillsProbeError, + FileSystem.FileSystem | Path.Path +> { + const path = yield* Path.Path; + const [configSkills, cliSkills] = antigravityUserSkillDirectories( + path, + path.join(input.userHome, ".gemini"), + ); + const skills = yield* scanSkillDirectories( + [ + { directory: configSkills, scope: "user" }, + { directory: path.resolve(input.cwd, ".gemini", "skills"), scope: "project" }, + { directory: cliSkills, scope: "user" }, + { directory: path.resolve(input.cwd, ".agents", "skills"), scope: "project" }, + { directory: path.resolve(input.cwd, ".agent", "skills"), scope: "project" }, + ], + { remainingBytes: MAX_SCAN_BYTES, remainingEntries: MAX_SCAN_ENTRIES }, + ); + const plugins = yield* discoverAntigravityPlugins(input.userHome); + return [...skills, ...plugins.flatMap((plugin) => plugin.skills)].sort((left, right) => + left.name.localeCompare(right.name), + ); +}); + +// ACP accepts standalone skills, so plugin entries use a process-owned native catalog. +export const prepareAntigravityPluginSkills = Effect.fn("prepareAntigravityPluginSkills")( + function* (input: { readonly userHome: string; readonly runtimeTempDirectory: string }) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const plugins = yield* discoverAntigravityPlugins(input.userHome); + if (plugins.length === 0) return []; + const directory = path.join(input.runtimeTempDirectory, "plugin-skills"); + yield* Effect.forEach( + plugins.flatMap((plugin) => + plugin.skills.map((skill) => ({ pluginDirectory: plugin.directory, skill })), + ), + ({ pluginDirectory, skill }, index) => + Effect.gen(function* () { + const skillDirectory = path.join(directory, ".agents", "skills", String(index)); + yield* fileSystem.makeDirectory(skillDirectory, { recursive: true }); + yield* fileSystem.writeFileString( + path.join(skillDirectory, "SKILL.md"), + `---\nname: ${encodeString(skill.name)}\ndescription: ${encodeString(skill.description ?? "")}\n---\n\nRead and follow the skill at ${encodeString(skill.path)}. Resolve its relative paths from ${encodeString(path.dirname(skill.path))}. The plugin root is ${encodeString(pluginDirectory)}.\n`, + ); + }), + { concurrency: 8, discard: true }, + ); + return [directory, ...plugins.map((plugin) => plugin.directory)]; + }, +); From 03c80e23e1a08168e3bb42a1326729c3cd6c7c5c Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:42:14 +0300 Subject: [PATCH 2/6] fix: keep valid Antigravity skills available Skip malformed plugin manifests without hiding valid skills and avoid case-colliding test directories. --- .../Drivers/AntigravitySkills.test.ts | 34 ++++++++++++++++--- .../src/provider/Drivers/AntigravitySkills.ts | 11 +++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 68a99ecb95088..5b64015b7e6f6 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -103,7 +103,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { const input = yield* makeWorkspace(); const config = path.join(input.userHome, ".gemini", "config"); const expected = []; - for (const name of ["alpha", "beta", "off", "disabled", "unregistered"]) { + for (const name of ["alpha", "beta", "broken", "off", "disabled", "unregistered"]) { const plugin = path.join(config, "plugins", name); const skillPath = yield* writeSkill( path.join(plugin, "skills", "review"), @@ -112,7 +112,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { if (name !== "unregistered") { yield* fileSystem.writeFileString( path.join(plugin, "plugin.json"), - name === "off" + name === "off" || name === "broken" ? "{ invalid" : JSON.stringify({ name: `Display ${name}`, @@ -135,6 +135,17 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { path.join(config, "config.json"), JSON.stringify({ plugins: { beta: { enabled: true }, off: { enabled: false } } }), ); + for (const [directory, scope] of [ + [path.join(input.cwd, ".agents", "skills"), "project"], + [path.join(input.userHome, ".gemini", "config", "skills"), "user"], + ] as const) { + const name = `standalone-${scope}`; + const skillPath = yield* writeSkill( + path.join(directory, name), + `---\nname: ${name}\n---\n`, + ); + expected.push({ name, path: skillPath, scope, enabled: true }); + } assert.deepEqual(yield* discoverAntigravitySkills(input), expected); const directories = yield* prepareAntigravityPluginSkills({ userHome: input.userHome, @@ -159,12 +170,12 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { }), ); - it.effect("keeps punctuation, case and prefix variants distinct and invocable", () => + it.effect("keeps punctuation and prefix variants distinct and invocable", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const input = yield* makeWorkspace(); - const directories = ["foo.bar", "foo_bar", "_alpha", "plugin_alpha", "alpha", "Alpha"]; + const directories = ["foo.bar", "foo_bar", "_alpha", "plugin_alpha", "alpha"]; for (const name of directories) { const plugin = path.join(input.userHome, ".gemini", "config", "plugins", name); yield* writeSkill(path.join(plugin, "skills", "review"), "---\nname: review\n---\n"); @@ -184,6 +195,21 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { }), ); + it.effect("preserves plugin directory case across separate homes", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const identifiers = []; + for (const name of ["alpha", "Alpha"]) { + const input = yield* makeWorkspace(); + const plugin = path.join(input.userHome, ".gemini", "config", "plugins", name); + yield* writeSkill(path.join(plugin, "skills", "review"), "---\nname: review\n---\n"); + yield* fileSystem.writeFileString(path.join(plugin, "plugin.json"), "{}"); + identifiers.push(...(yield* discoverAntigravitySkills(input)).map((skill) => skill.name)); + } + assert.deepEqual(identifiers, ["plugin:alpha:review", "plugin:_41_lpha:review"]); + }), + ); it.effect("uses native root order for duplicate names", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index 951e98e676327..fac525d713966 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -268,11 +268,14 @@ const discoverAntigravityPlugins = Effect.fn("discoverAntigravityPlugins")(funct const contents = yield* readSkill(manifestPath, budget); if (contents === undefined) continue; const manifest = yield* decodePluginManifest(contents).pipe( - Effect.mapError( - (cause) => - new AntigravitySkillsProbeError({ reason: "invalid-config", path: manifestPath, cause }), - ), + Effect.catchTags({ + SchemaError: () => + Effect.logWarning("Skipped an Antigravity plugin with an invalid manifest.", { + errorTag: "invalid-config", + }).pipe(Effect.as(undefined)), + }), ); + if (manifest === undefined) continue; if (manifest.disabled && enabled !== true) continue; const skills = yield* scanSkillDirectories( [{ directory: path.join(pluginDirectory, "skills"), scope: "user" }], From c4e03f972450f6c598dcedd592f985fd9b578f8a Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Wed, 4 Nov 2026 04:09:37 +0300 Subject: [PATCH 3/6] fix: isolate invalid plugin preferences Skip invalid plugin preferences in the shared parser so standalone skills remain available. --- .../Drivers/AntigravityDriver.test.ts | 4 ++-- .../Drivers/AntigravitySkills.test.ts | 4 ++++ .../src/provider/Drivers/AntigravitySkills.ts | 21 ++++++++----------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 30a925295007a..5d040b11f8645 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -280,13 +280,13 @@ const layerTest = ProviderHostLive.layer.pipe( it.layer(layerTest)("AntigravityDriver", (it) => { it.effect.skipIf(windowsHost)( - "refreshes through the selected executable and personal Google ACP", + "refreshes through the selected executable despite plugin preparation failure", () => Effect.gen(function* () { const h = yield* makeHarness(); const plugin = h.path.join(h.userHome, ".gemini", "config", "plugins", "broken"); yield* h.fs.makeDirectory(plugin, { recursive: true }); - yield* h.fs.writeFileString(h.path.join(plugin, "plugin.json"), "{ invalid"); + yield* h.fs.writeFileString(h.path.join(plugin, "plugin.json"), "x".repeat(1_000_001)); yield* h.refresh(); const snapshot = yield* h.instance.snapshot.getSnapshot; expect(snapshot.status).toBe("ready"); diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 5b64015b7e6f6..8d8f73ddd36b1 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -167,6 +167,10 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { ); assert.include(yield* fileSystem.readFileString(expected[index]!.path), "name: review\n"); } + for (const contents of ["{ invalid", '{"plugins":{"alpha":{"enabled":"yes"}}}']) { + yield* fileSystem.writeFileString(path.join(config, "config.json"), contents); + assert.deepEqual(yield* discoverAntigravitySkills(input), expected.slice(2)); + } }), ); diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index fac525d713966..881655ea80f9c 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -47,7 +47,7 @@ const decodeSkillFrontmatter = Schema.decodeUnknownSync(SkillFrontmatter); export class AntigravitySkillsProbeError extends Schema.TaggedError()( "AntigravitySkillsProbeError", { - reason: Schema.Literals(["scan-budget-exhausted", "filesystem-error", "invalid-config"]), + reason: Schema.Literals(["scan-budget-exhausted", "filesystem-error"]), path: Schema.String, cause: Schema.optional(Schema.Defect()), }, @@ -55,9 +55,7 @@ export class AntigravitySkillsProbeError extends Schema.TaggedError - new AntigravitySkillsProbeError({ - reason: "invalid-config", - path: preferencesPath, - cause, - }), - ), + Effect.catchTags({ + SchemaError: () => + Effect.logWarning("Antigravity plugin preferences are invalid.", { + errorTag: "invalid-config", + }).pipe(Effect.as(undefined)), + }), ); + if (preferences === undefined) return []; const plugins = []; for (const entry of entries.toSorted()) { const pluginDirectory = path.join(directory, entry); From 302badac71c3651793d457446644cb7531e83a0f Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:50:41 +0300 Subject: [PATCH 4/6] test: compare encoded plugin skill paths Match the JSON path representation so backslashes do not break the assertion on Windows. --- apps/server/src/provider/Drivers/AntigravitySkills.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 8d8f73ddd36b1..d77224eb9cfa6 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -160,7 +160,10 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { ["plugin:alpha:review", "plugin:beta:review"], ); for (const [index, skill] of nativeSkills.entries()) { - assert.include(yield* fileSystem.readFileString(skill.path), expected[index]!.path); + assert.include( + yield* fileSystem.readFileString(skill.path), + JSON.stringify(expected[index]!.path), + ); assert.equal( directories[index + 1], path.dirname(path.dirname(path.dirname(expected[index]!.path))), From eb5a7298c95c68737d43c11e99538e96bb9d66f8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 22:46:57 +0000 Subject: [PATCH 5/6] fix(server): read Antigravity plugin config in full after main merge Main replaced the whole-file skill reader with a frontmatter-only reader, which would cut plugin.json and config.json at a second '---'. Read those JSON files with their own budgeted reader, and drop a test the punctuation-variant case already covers. Co-Authored-By: Claude Opus 5.5 --- .../Drivers/AntigravitySkills.test.ts | 17 +------------- .../src/provider/Drivers/AntigravitySkills.ts | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 64d3edc9b973c..1f3bced88ebc0 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -115,7 +115,7 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { name === "off" || name === "broken" ? "{ invalid" : JSON.stringify({ - name: `Display ${name}`, + name: `--- Display ${name} ---`, disabled: name === "beta" || name === "disabled", }), ); @@ -202,21 +202,6 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { }), ); - it.effect("preserves plugin directory case across separate homes", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const identifiers = []; - for (const name of ["alpha", "Alpha"]) { - const input = yield* makeWorkspace(); - const plugin = path.join(input.userHome, ".gemini", "config", "plugins", name); - yield* writeSkill(path.join(plugin, "skills", "review"), "---\nname: review\n---\n"); - yield* fileSystem.writeFileString(path.join(plugin, "plugin.json"), "{}"); - identifiers.push(...(yield* discoverAntigravitySkills(input)).map((skill) => skill.name)); - } - assert.deepEqual(identifiers, ["plugin:alpha:review", "plugin:_41_lpha:review"]); - }), - ); it.effect("uses native root order for duplicate names", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index f4f54cf1b0510..fe69ac6d97e6e 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -227,6 +227,25 @@ const scanSkillDirectories = Effect.fn("scanAntigravitySkillDirectories")(functi return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name)); }); +const readJsonFile = Effect.fn("readAntigravityPluginConfig")(function* ( + filePath: string, + budget: ScanBudget, +) { + const fileSystem = yield* FileSystem.FileSystem; + const info = yield* readIfPresent(fileSystem.stat(filePath), filePath); + if (info?.type !== "File") return undefined; + if (info.size > BigInt(Math.min(MAX_SKILL_BYTES, budget.remainingBytes))) { + return yield* new AntigravitySkillsProbeError({ + reason: "scan-budget-exhausted", + path: filePath, + }); + } + const contents = yield* readIfPresent(fileSystem.readFileString(filePath), filePath); + if (contents === undefined) return undefined; + budget.remainingBytes -= Buffer.byteLength(contents); + return contents; +}); + const PluginManifest = Schema.fromJsonString( Schema.Struct({ disabled: Schema.optional(Schema.Boolean) }), ); @@ -263,7 +282,7 @@ const discoverAntigravityPlugins = Effect.fn("discoverAntigravityPlugins")(funct } budget.remainingEntries -= entries.length; const preferencesPath = path.join(config, "config.json"); - const preferencesContents = yield* readSkill(preferencesPath, budget); + const preferencesContents = yield* readJsonFile(preferencesPath, budget); const preferences = preferencesContents === undefined ? { plugins: {} } @@ -287,7 +306,7 @@ const discoverAntigravityPlugins = Effect.fn("discoverAntigravityPlugins")(funct const enabled = preferences.plugins?.[entry]?.enabled; if (enabled === false) continue; const manifestPath = path.join(pluginDirectory, "plugin.json"); - const contents = yield* readSkill(manifestPath, budget); + const contents = yield* readJsonFile(manifestPath, budget); if (contents === undefined) continue; const manifest = yield* decodePluginManifest(contents).pipe( Effect.catchTags({ From ac5e7daf0741d68244a01d55c98cf3986288476e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 23:03:09 +0000 Subject: [PATCH 6/6] fix(server): let Antigravity client reads reach prepared plugin skills The driver passed plugin skill directories to the agent, but T3's fs/read_text_file handler only allowed the workspace and attachments, so reading a plugin wrapper, its original SKILL.md, or a resource beside it failed as outside the session workspace. The driver now returns those roots on the runtime, and the adapter retries a refused read confined to them. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AntigravityAdapterV2.test.ts | 37 +++++++++++++++++-- .../Adapters/AntigravityAdapterV2.ts | 15 +++++++- .../src/provider/Drivers/AntigravityDriver.ts | 5 ++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index b281ecc16ab84..d9d2767db4890 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -36,6 +36,7 @@ import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSession import { makeAntigravityAcpRuntime } from "../../provider/acp/AntigravityAcpSupport.ts"; import { normalizeAntigravitySessionUpdate } from "../../provider/acp/AntigravityProtocol.ts"; import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE } from "../../provider/antigravityAuthSupport.ts"; +import { prepareAntigravityPluginSkills } from "../../provider/Drivers/AntigravitySkills.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import { @@ -292,6 +293,20 @@ describe("AntigravityAdapterV2 client file system", () => { const requestLog = path.join(logs, "requests.ndjson"); const crypto = yield* SecureRandom.SecureRandom; const instanceId = ProviderInstanceId.make("antigravity-containment-test"); + const userHome = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-antigravity-home-", + }); + const plugin = path.join(userHome, ".gemini", "config", "plugins", "alpha"); + const pluginSkill = path.join(plugin, "skills", "review", "SKILL.md"); + const pluginResource = path.join(path.dirname(pluginSkill), "checklist.md"); + yield* fileSystem.makeDirectory(path.dirname(pluginSkill), { recursive: true }); + yield* fileSystem.writeFileString(pluginSkill, "---\nname: review\n---\nUse checklist.md\n"); + yield* fileSystem.writeFileString(pluginResource, "checklist"); + yield* fileSystem.writeFileString(path.join(plugin, "plugin.json"), "{}"); + const clientFileRoots = yield* prepareAntigravityPluginSkills({ + userHome, + runtimeTempDirectory: yield* fileSystem.makeTempDirectoryScoped(), + }); const adapter = yield* makeAntigravityAdapterV2({ instanceId, selfInvocation: yield* resolveSelfInvocation(), @@ -307,13 +322,16 @@ describe("AntigravityAdapterV2 client file system", () => { }, }).pipe( Effect.provideService(SecureRandom.SecureRandom, crypto), - Effect.map((runtime): RuntimeService => ({ + Effect.map((runtime) => ({ ...runtime, - handleReadTextFile: (handler) => + clientFileRoots, + handleReadTextFile: (handler: Parameters[0]) => Effect.sync(() => { readTextFile = handler; }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))), - handleWriteTextFile: (handler) => + handleWriteTextFile: ( + handler: Parameters[0], + ) => Effect.sync(() => { writeTextFile = handler; }).pipe(Effect.andThen(runtime.handleWriteTextFile(handler))), @@ -416,6 +434,19 @@ describe("AntigravityAdapterV2 client file system", () => { context("fs/read_text_file"), ); assert.equal(viaInsideLink.content, "inside"); + + const wrapper = path.join(clientFileRoots[0]!, ".agents", "skills", "0", "SKILL.md"); + for (const [file, expected] of [ + [wrapper, JSON.stringify(pluginSkill)], + [pluginSkill, "Use checklist.md"], + [pluginResource, "checklist"], + ] as const) { + const read = yield* readTextFile( + { sessionId: "mock-session-1", path: file }, + context("fs/read_text_file"), + ); + assert.include(read.content, expected); + } }).pipe(Effect.provide(layerSession)), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts index b6f746269a6f2..2b2df0e13784c 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts @@ -76,7 +76,10 @@ export interface AntigravityAdapterV2Options { readonly makeRuntime: ( input: Omit, ) => Effect.Effect< - AcpSessionRuntime.AcpSessionRuntime["Service"], + AcpSessionRuntime.AcpSessionRuntime["Service"] & { + /** Session-local roots outside the workspace, such as prepared plugin skills. */ + readonly clientFileRoots?: ReadonlyArray; + }, EffectAcpErrors.AcpError | ProviderSetupError, Scope.Scope >; @@ -161,6 +164,16 @@ export function makeAntigravityAcpAdapterFlavor( handleReadTextFile: (handler) => runtime.handleReadTextFile((request, context) => handler(request, context).pipe( + Effect.catch((cause) => + runtime.clientFileRoots?.length + ? readAntigravityClientTextFile({ + fileSystem: options.fileSystem, + path: options.path, + allowedRoots: runtime.clientFileRoots, + request, + }).pipe(Effect.mapError(() => cause)) + : Effect.fail(cause), + ), Effect.onExit((exit) => { const served = reads.attempted(request.path, Exit.isSuccess(exit)); if (served === undefined || projectUpdate === undefined) return Effect.void; diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 98453492492c5..0b2ef5deadcdb 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -204,7 +204,9 @@ export const AntigravityDriver: ProviderDriver< const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* ( input: Omit, ): Effect.fn.Return< - AcpSessionRuntime.AcpSessionRuntime["Service"], + AcpSessionRuntime.AcpSessionRuntime["Service"] & { + readonly clientFileRoots: ReadonlyArray; + }, AcpErrors.AcpError | ProviderSetupError, Scope.Scope > { @@ -300,6 +302,7 @@ export const AntigravityDriver: ProviderDriver< }).pipe(Effect.provideService(SecureRandom.SecureRandom, random)); return { ...runtime, + clientFileRoots: pluginDirectories, ...(executable.harnessPath === null ? { setMode: (modeId: string) => runtime.setMode(antigravityCliSessionMode(modeId)) } : {}),