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 <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
cynarAI 2026-09-22 11:22:14 +02:00 committed by GitHub
parent 04528a93e3
commit ff11f1d151
2 changed files with 91 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import {
toolCompletionAuditPayload, toolCompletionAuditPayload,
toolCompletionFromResult, toolCompletionFromResult,
} from "./executor.js"; } from "./executor.js";
import { agentSecretScopeForRun } from "./agent-environment.js";
import { missingTurnImagesInstruction, settleSteeringAttachmentLoads } from "./executor-attachments.js"; import { missingTurnImagesInstruction, settleSteeringAttachmentLoads } from "./executor-attachments.js";
import { threadContextForRun } from "./executor-completion.js"; import { threadContextForRun } from "./executor-completion.js";
import { serializeModelSecret } from "./pi-oauth.js"; import { serializeModelSecret } from "./pi-oauth.js";
@ -1253,6 +1254,94 @@ description: Prepare standup notes
expect(runtimeRun).not.toHaveBeenCalled(); 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<typeof createRunExecutor>[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 () => { it("resolves a per-bot model override with that provider’s credential", async () => {
const findFirst = vi.fn( const findFirst = vi.fn(
async (args: { where: { credential?: { provider?: string }; isDefault?: boolean } }) => { async (args: { where: { credential?: { provider?: string }; isDefault?: boolean } }) => {

View File

@ -108,6 +108,7 @@ import {
respondAgentConnection, respondAgentConnection,
} from "./agent-connections.js"; } from "./agent-connections.js";
import { import {
agentSecretScopeForRun,
decryptAgentEnvironment, decryptAgentEnvironment,
formatAgentEnvironmentInstruction, formatAgentEnvironmentInstruction,
redactAgentCommandResult, redactAgentCommandResult,
@ -1026,7 +1027,7 @@ export function createRunExecutor(deps: ExecutorDeps) {
userId: run.userId, userId: run.userId,
}), }),
deps.prisma.agentSecret.findMany({ deps.prisma.agentSecret.findMany({
where: { spaceId: run.spaceId }, where: agentSecretScopeForRun(run.spaceId, run.userId),
select: { select: {
name: true, name: true,
secret: { select: { id: true, ciphertext: true } }, secret: { select: { id: true, ciphertext: true } },