diff --git a/src/adversary/agents/blue-team.ts b/src/adversary/agents/blue-team.ts index fe92432..09c5d5a 100644 --- a/src/adversary/agents/blue-team.ts +++ b/src/adversary/agents/blue-team.ts @@ -32,7 +32,9 @@ export function BlueTeam() { env as unknown as AdversarySandboxEnv, input.sandboxId, ); - useSandbox(adversaryAgentSandbox(sandbox, BLUE_DIR), { cwd: BLUE_DIR }); + useSandbox(adversaryAgentSandbox(sandbox, BLUE_DIR, input.skill.name), { + cwd: BLUE_DIR, + }); const writeResult = useDataWriter('result', { schema: blueTeamResultSchema }); useTool({ diff --git a/src/adversary/agents/purple-team.ts b/src/adversary/agents/purple-team.ts index 67238c3..7e98285 100644 --- a/src/adversary/agents/purple-team.ts +++ b/src/adversary/agents/purple-team.ts @@ -34,7 +34,9 @@ export function PurpleTeam() { env as unknown as AdversarySandboxEnv, input.sandboxId, ); - useSandbox(adversaryAgentSandbox(sandbox, RED_DIR), { cwd: RED_DIR }); + useSandbox(adversaryAgentSandbox(sandbox, RED_DIR, input.skill.name), { + cwd: RED_DIR, + }); const writeResult = useDataWriter('result', { schema: purpleTeamResultSchema, diff --git a/src/adversary/sandbox.ts b/src/adversary/sandbox.ts index 3ea59d8..916358d 100644 --- a/src/adversary/sandbox.ts +++ b/src/adversary/sandbox.ts @@ -56,14 +56,24 @@ export function getAdversarySandbox( export function adversaryAgentSandbox( sandbox: AdversarySandbox, cwd: string, + mountedSkillName: string, ): SandboxFactory { const base = cloudflareSandbox(sandbox, { cwd }); + const workspaceSkillsDir = `${cwd}/.agents/skills`; return { ...base, async createSandbox(options) { const environment = await base.createSandbox(options); return { ...environment, + async readdir(path) { + const entries = await environment.readdir(path); + // The pinned snapshot is mounted with useSkill; hide its checkout + // copy from Flue's workspace discovery to avoid a name collision. + return path === workspaceSkillsDir + ? entries.filter((entry) => entry !== mountedSkillName) + : entries; + }, exec(command, execOptions) { const requested = execOptions?.timeoutMs ?? COMMAND_TIMEOUT_SECONDS * 1_000; diff --git a/tests/adversary-sandbox.test.ts b/tests/adversary-sandbox.test.ts new file mode 100644 index 0000000..ca45313 --- /dev/null +++ b/tests/adversary-sandbox.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + readdir: vi.fn<(path: string) => Promise>(), +})); + +vi.mock('@cloudflare/sandbox', () => ({ getSandbox: vi.fn() })); +vi.mock('@flue/runtime/cloudflare', () => ({ + cloudflareSandbox: vi.fn(() => ({ + createSandbox: vi.fn(async () => ({ + cwd: '/', + exec: vi.fn(), + readdir: mocks.readdir, + })), + })), +})); + +import { + type AdversarySandbox, + adversaryAgentSandbox, + BLUE_DIR, + RED_DIR, +} from '../src/adversary/sandbox.ts'; + +describe('adversary agent sandbox', () => { + it.each([ + [BLUE_DIR, 'astro-adversary-blue'], + [RED_DIR, 'astro-adversary-purple'], + ])( + 'hides the mounted skill in %s from workspace discovery', + async (cwd, skill) => { + const skillsDir = `${cwd}/.agents/skills`; + mocks.readdir.mockImplementation(async (path) => + path === skillsDir ? [skill, 'other-skill'] : [skill], + ); + const factory = adversaryAgentSandbox({} as AdversarySandbox, cwd, skill); + const environment = await factory.createSandbox({ id: 'test' }); + + expect(await environment.readdir(skillsDir)).toEqual(['other-skill']); + expect(await environment.readdir(`${cwd}/src`)).toEqual([skill]); + }, + ); +});