From ff11f1d1512f3f7ccaf5772d643df260c6d61882 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:22:14 +0200 Subject: [PATCH] fix(adapters): scope agent secrets to run owner in shared Spaces (reland #47) (#70) The executor loaded every agent secret in a Space and injected decrypted values into shell environment variables for any member's bot run. Restore agentSecretScopeForRun scoping accidentally dropped in 0a8a831. Add an executor test that fails when findMany is scoped only by spaceId. Co-authored-by: Cursor Agent Co-authored-by: cynarAI --- packages/adapters/src/executor.test.ts | 89 ++++++++++++++++++++++++++ packages/adapters/src/executor.ts | 3 +- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/adapters/src/executor.test.ts b/packages/adapters/src/executor.test.ts index 38ce1f7..d35d63c 100644 --- a/packages/adapters/src/executor.test.ts +++ b/packages/adapters/src/executor.test.ts @@ -12,6 +12,7 @@ import { toolCompletionAuditPayload, toolCompletionFromResult, } from "./executor.js"; +import { agentSecretScopeForRun } from "./agent-environment.js"; import { missingTurnImagesInstruction, settleSteeringAttachmentLoads } from "./executor-attachments.js"; import { threadContextForRun } from "./executor-completion.js"; import { serializeModelSecret } from "./pi-oauth.js"; @@ -1253,6 +1254,94 @@ description: Prepare standup notes expect(runtimeRun).not.toHaveBeenCalled(); }); + it("scopes agent secrets to the run owner when loading the environment", async () => { + const agentSecretFindMany = vi.fn(async () => []); + const run = { + id: "run-1", + botId: "bot-1", + threadId: "thread-1", + taskId: "task-1", + userId: "member-user", + spaceId: "shared-ws", + status: "queued", + trigger: "user", + routineId: null, + sourceMessageId: null, + checkpoint: null, + leaseFence: 0, + }; + const botLookup = vi.fn(async (args: { select?: { computerId?: boolean } }) => + args.select?.computerId + ? { computerId: "computer-1", computerSwitching: false } + : { + id: "bot-1", + name: "Assistant", + modelProvider: null, + modelId: null, + thinkingLevel: null, + memoryScope: "isolated", + computer: { id: "computer-1", scope: "private" }, + }, + ); + const prisma = { + run: { + findUnique: vi.fn(async () => run), + findUniqueOrThrow: vi.fn(async () => ({ status: "leased", startedAt: null })), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + bot: { findUniqueOrThrow: botLookup }, + computer: { + findUniqueOrThrow: vi.fn(async () => ({ scope: "private", state: "running" })), + }, + attempt: { + create: vi.fn(async () => ({ id: "attempt-1" })), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + thread: { + findUniqueOrThrow: vi.fn(async () => ({ + id: "thread-1", + groupId: null, + historyCompactionSummary: null, + historyCompactedUpToSeq: null, + historyCompactionGeneration: 0, + })), + }, + message: { findMany: vi.fn(async () => []) }, + task: { findUniqueOrThrow: vi.fn(async () => ({ id: "task-1", prompt: "hello" })) }, + connection: { findMany: vi.fn(async () => []) }, + spaceModelPreference: { findFirst: vi.fn(async () => null) }, + userModelCredential: { findFirst: vi.fn(async () => null) }, + deploymentSettings: { findUnique: vi.fn(async () => null) }, + taughtSkill: { findMany: vi.fn(async () => []) }, + agentSecret: { findMany: agentSecretFindMany }, + agentSkill: { findMany: vi.fn(async () => []) }, + scratchpadItem: { findMany: vi.fn(async () => []) }, + } as unknown as PrismaClient; + const executor = createRunExecutor({ + prisma, + runtime: { + describe: () => ({ capabilities: { scripted: false } }), + run: vi.fn(), + }, + memoryProviders: { resolve: vi.fn(async () => null) }, + memory: { read: vi.fn(async () => ({ documents: [] })) }, + events: { + append: vi.fn(async () => undefined), + finalizeRun: vi.fn(async () => ({ continuationRunId: null })), + }, + jobs: { enqueue: vi.fn(async () => undefined) }, + secrets: [], + } as unknown as Parameters[0]); + + await executor.continueRun("run-1", "worker-1"); + + expect(agentSecretFindMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: agentSecretScopeForRun("shared-ws", "member-user"), + }), + ); + }); + it("resolves a per-bot model override with that provider’s credential", async () => { const findFirst = vi.fn( async (args: { where: { credential?: { provider?: string }; isDefault?: boolean } }) => { diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 3121e7c..4164e0e 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -108,6 +108,7 @@ import { respondAgentConnection, } from "./agent-connections.js"; import { + agentSecretScopeForRun, decryptAgentEnvironment, formatAgentEnvironmentInstruction, redactAgentCommandResult, @@ -1026,7 +1027,7 @@ export function createRunExecutor(deps: ExecutorDeps) { userId: run.userId, }), deps.prisma.agentSecret.findMany({ - where: { spaceId: run.spaceId }, + where: agentSecretScopeForRun(run.spaceId, run.userId), select: { name: true, secret: { select: { id: true, ciphertext: true } },