Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ it.effect(
shareProfile: false,
},
});
expect(instance.sharedSkillsDirectory).toBeUndefined();
expect((yield* instance.snapshot.refresh).auth.email).toBe("before@example.test");
expect((yield* instance.snapshot.refresh).auth.email).toBe("before@example.test");
const auth = instance.auth!;
Expand Down Expand Up @@ -206,6 +207,7 @@ for (const conflict of [false, true]) {
expect(yield* fs.readLink(path.join(account, "settings.json"))).toBe(
path.join(shared, "settings.json"),
);
expect(instance.sharedSkillsDirectory).toBe(path.join(shared, "skills"));
expect(instance.continuationIdentity.continuationKey).toBe(`claude:home:${account}`);
expect(instance.auth?.credentialBinding).toEqual({ owner: "provider", key: account });
}
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ export const ClaudeDriver: ProviderDriver<
return {
instanceId,
driverKind: DRIVER_KIND,
...(runtime.overlay?.active === false
? {}
: { sharedSkillsDirectory: path.join(runtime.overlay?.sharedRoot ?? configDir, "skills") }),
continuationIdentity: {
...fallbackContinuationIdentity,
continuationKey: continuationGroupKey,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/provider/Drivers/CodexDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ it.layer(layerTest)("CodexDriver", (it) => {
),
),
);
expect(instance.sharedSkillsDirectory).toBe(NodePath.join(sharedHome, "skills"));
const observedAccount = yield* Deferred.make<void>();
const disconnected = yield* instance.snapshot.streamChanges.pipe(
Stream.tap((provider) =>
Expand Down Expand Up @@ -568,6 +569,7 @@ it.layer(layerTest)("CodexDriver", (it) => {
environment: [{ name: "CODEX_HOME", value: codexHome, sensitive: false }],
config: CodexDriver.defaultConfig(),
});
expect(instance.sharedSkillsDirectory).toBe(NodePath.join(codexHome, "skills"));
expect(instance.continuationIdentity.continuationKey).toBe(`codex:home:${codexHome}`);
}
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), Effect.scoped),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ export const CodexDriver: ProviderDriver<
return {
instanceId,
driverKind: DRIVER_KIND,
sharedSkillsDirectory: pathService.join(homeLayout.sharedHomePath, "skills"),
continuationIdentity,
displayName,
accentColor,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Drivers/CodexManagedProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu
return {
instanceId,
driverKind: DRIVER,
sharedSkillsDirectory: path.join(runtime.homeLayout.sharedHomePath, "skills"),
continuationIdentity,
displayName,
accentColor,
Expand Down
244 changes: 215 additions & 29 deletions apps/server/src/skills/SkillsLibrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.
import { discoverAntigravitySkills } from "../provider/Drivers/AntigravitySkills.ts";
import { prepareAntigravityProfile } from "../provider/antigravityAuthSupport.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as ServerConfig from "../config.ts";
import * as SkillsLibrary from "./SkillsLibrary.ts";

const setup = Effect.fn("SkillsLibrary.test.setup")(function* (
drivers: ReadonlyArray<ProviderDriverKind> = [],
configuredRoot?: { readonly directory: string; readonly enabled: boolean },
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand All @@ -28,35 +30,39 @@ const setup = Effect.fn("SkillsLibrary.test.setup")(function* (
const library = path.join(home, ".agents", "skills");
yield* fs.makeDirectory(claude, { recursive: true });
yield* fs.makeDirectory(codex, { recursive: true });
const service = yield* SkillsLibrary.make.pipe(
Effect.provide(
Layer.mergeAll(
ServerSettings.ServerSettingsService.layerTest(),
Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({
listInstances: Effect.succeed(
drivers.map((driverKind) => ({
instanceId: ProviderInstanceId.make(driverKind),
driverKind,
enabled: true,
displayName: undefined,
continuationIdentity: { driverKind, continuationKey: driverKind },
get orchestrationAdapter(): never {
throw new Error("Sharing skills must not start a provider session.");
},
get snapshot(): never {
throw new Error("Sharing skills must not probe a provider.");
},
get textGeneration(): never {
throw new Error("Sharing skills must not generate text.");
},
})),
),
}),
const reopen = (root: typeof configuredRoot, stateHome = path.join(home, "environment")) =>
SkillsLibrary.make.pipe(
Effect.provide(
Layer.mergeAll(
ServerSettings.ServerSettingsService.layerTest(),
ServerConfig.layerTest(home, stateHome),
Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({
listInstances: Effect.succeed(
drivers.map((driverKind) => ({
instanceId: ProviderInstanceId.make(driverKind),
driverKind,
enabled: root?.enabled ?? true,
...(root ? { sharedSkillsDirectory: root.directory } : {}),
displayName: undefined,
continuationIdentity: { driverKind, continuationKey: driverKind },
get orchestrationAdapter(): never {
throw new Error("Sharing skills must not start a provider session.");
},
get snapshot(): never {
throw new Error("Sharing skills must not probe a provider.");
},
get textGeneration(): never {
throw new Error("Sharing skills must not generate text.");
},
})),
),
}),
),
),
),
Effect.provideService(HostProcess.HomeDirectory, home),
Effect.provideService(HostProcess.Environment, { HOME: home }),
);
Effect.provideService(HostProcess.HomeDirectory, home),
Effect.provideService(HostProcess.Environment, { HOME: home }),
);
const service = yield* reopen(configuredRoot);
const skill = (root: string, name: string, body: string, frontmatterName = name) =>
fs
.makeDirectory(path.join(root, name), { recursive: true })
Expand All @@ -71,7 +77,7 @@ const setup = Effect.fn("SkillsLibrary.test.setup")(function* (
const read = (root: string, name: string) => fs.readFileString(path.join(root, name, "SKILL.md"));
const link = (root: string, name: string) =>
fs.readLink(path.join(root, name)).pipe(Effect.orElseSucceed(() => undefined));
return { fs, path, home, claude, codex, library, service, skill, read, link };
return { fs, path, home, claude, codex, library, service, reopen, skill, read, link };
});

/** Every entry under `root`, links not followed, with what a write would change. */
Expand All @@ -92,6 +98,100 @@ const snapshot = (root: string) =>

it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
describe("apply", () => {
it.effect.skipIf(!symlinksSupported)(
"shares a configured profile root once and restores it when disabled",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const customHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-custom-skills-" });
const directory = path.join(customHome, "skills");
const { service, library, skill, read, link } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* skill(directory, "custom", "custom instructions");
yield* skill(library, "shared", "shared instructions");
yield* service.apply;
yield* service.apply;
expect(yield* read(library, "custom")).toContain("custom instructions");
expect(yield* read(directory, "shared")).toContain("shared instructions");
expect(yield* link(directory, "custom")).toBe(path.join(library, "custom"));
yield* service.detach;
expect(yield* link(directory, "custom")).toBeUndefined();
expect(yield* read(directory, "shared")).toContain("shared instructions");
yield* service.detach;
}),
);

it.effect.skipIf(!symlinksSupported)(
"deduplicates a configured alias of the default profile",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const customHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-alias-skills-" });
const directory = path.join(customHome, "skills");
const { service, library, claude, skill, read } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* fs.symlink(claude, directory);
yield* skill(library, "shared", "shared instructions");
yield* service.apply;
expect(
(yield* service.status).linked.filter((entry) => entry.driver === "claudeAgent"),
).toEqual([{ driver: "claudeAgent", name: "shared" }]);
yield* service.detach;
expect(yield* read(directory, "shared")).toContain("shared instructions");
}),
);

it.effect.skipIf(!symlinksSupported)(
"recognizes the library through an alias before its skills directory exists",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const customHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-library-alias-" });
const alias = path.join(customHome, "agents");
const directory = path.join(alias, "skills");
const { service, library, claude, skill, read } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* fs.makeDirectory(path.dirname(library), { recursive: true });
yield* fs.symlink(path.dirname(library), alias);
yield* skill(claude, "custom", "custom instructions");
yield* service.apply;
expect(yield* read(library, "custom")).toContain("custom instructions");
yield* service.detach;
expect(yield* read(claude, "custom")).toContain("custom instructions");
}),
);

it.effect.skipIf(!symlinksSupported)(
"does not mutate a disabled account's configured profile",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const customHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-disabled-skills-" });
const directory = path.join(customHome, "skills");
const { service, library, skill, link } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: false },
);
yield* skill(directory, "custom", "private instructions");
yield* skill(library, "shared", "shared instructions");
const before = yield* snapshot(customHome);
yield* service.apply;
yield* service.detach;
expect(yield* snapshot(customHome)).toEqual(before);
expect(yield* link(directory, "custom")).toBeUndefined();
}),
);

it.effect.skipIf(!symlinksSupported)(
"makes shared skills available to a configured Antigravity with no global home",
() =>
Expand Down Expand Up @@ -306,6 +406,91 @@ it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
});

describe("detach", () => {
it.effect.skipIf(!symlinksSupported)(
"does not use another environment's ownership journal at startup",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const profiles = yield* fs.makeTempDirectoryScoped({ prefix: "t3-isolated-skills-" });
const directory = path.join(profiles, "skills");
const { service, reopen, home, claude, library, skill, link } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* skill(library, "shared", "shared instructions");
yield* service.apply;
const otherEnvironment = yield* reopen(undefined, path.join(home, "other-environment"));
yield* otherEnvironment.finishDetach;
expect(yield* link(directory, "shared")).toBe(path.join(library, "shared"));
expect(yield* link(claude, "shared")).toBe(path.join(library, "shared"));
yield* (yield* reopen(undefined)).finishDetach;
expect(yield* link(directory, "shared")).toBeUndefined();
}),
);

it.effect.skipIf(!symlinksSupported)(
"restores former configured homes after reassignment and restart",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const profiles = yield* fs.makeTempDirectoryScoped({ prefix: "t3-old-skills-" });
const directory = path.join(profiles, "old", "skills");
const replacement = path.join(profiles, "new", "skills");
const { service, reopen, library, skill, read, link } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* skill(library, "shared", "shared instructions");
yield* service.apply;
const changed = yield* reopen({ directory: replacement, enabled: true });
yield* changed.apply;
expect(yield* link(directory, "shared")).toBeUndefined();
expect(yield* read(directory, "shared")).toContain("shared instructions");
yield* skill(directory, "shared", "edited outside the library");
expect((yield* changed.status).conflicts).toEqual([]);
expect(yield* link(replacement, "shared")).toBe(path.join(library, "shared"));
yield* skill(replacement, ".shared.t3-restore", "unrelated staged folder");
const restartedWithoutProfile = yield* reopen(undefined);
yield* restartedWithoutProfile.finishDetach;
expect(yield* link(replacement, "shared")).toBe(path.join(library, "shared"));
expect(yield* read(replacement, ".shared.t3-restore")).toContain(
"unrelated staged folder",
);
yield* fs.remove(path.join(replacement, ".shared.t3-restore"), { recursive: true });
yield* (yield* reopen(undefined)).finishDetach;
expect(yield* link(replacement, "shared")).toBeUndefined();
expect(yield* read(replacement, "shared")).toContain("shared instructions");
}),
);

it.effect.skipIf(!symlinksSupported)(
"leaves a missing or reassigned former root alone during cleanup",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const profiles = yield* fs.makeTempDirectoryScoped({ prefix: "t3-missing-skills-" });
const directory = path.join(profiles, "skills");
const { service, reopen, library, skill } = yield* setup(
[ProviderDriverKind.make("claudeAgent")],
{ directory, enabled: true },
);
yield* skill(library, "shared", "shared instructions");
yield* service.apply;
yield* fs.remove(directory, { recursive: true });
yield* (yield* reopen(undefined)).detach;
expect(yield* fs.exists(directory)).toBe(false);
yield* service.apply;
yield* fs.remove(directory, { recursive: true });
yield* skill(directory, "shared", "new owner");
const before = yield* snapshot(profiles);
yield* (yield* reopen(undefined)).detach;
expect(yield* snapshot(profiles)).toEqual(before);
}),
);

it.effect.skipIf(!symlinksSupported)(
"restores real folders and removes only links into the library",
() =>
Expand Down Expand Up @@ -387,6 +572,7 @@ it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
Effect.provide(
Layer.mergeAll(
ServerSettings.ServerSettingsService.layerTest(),
ServerConfig.layerTest(home, path.join(home, "environment")),
Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({
listInstances: Effect.succeed([]),
subscribeChanges: PubSub.unbounded<void>().pipe(Effect.flatMap(PubSub.subscribe)),
Expand Down
Loading
Loading