import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { ComposioEmulator, LocalAgentHomeStore, teamBotWorkspaceDirectory } from "@rakazo/adapters"; import type { AiConsentStatus, appContract, Space, SpaceNavigation } from "@rakazo/contracts"; import { claimEmptySpaceDeletionForMember, computerHomeKey, createThreadMessage, deleteEmptySpaceForMember, releaseSpaceDeletionClaim, renewSpaceDeletionClaim, } from "@rakazo/db"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { createApp } from "../../../apps/api/src/app.ts"; import { sessionCookieHeader } from "./index.js"; type App = { request: (input: string, init?: RequestInit) => Response | Promise }; type AppHandles = Awaited>; type RpcPath = T extends { "~orpc": unknown } ? Prefix : T extends object ? { [Key in keyof T & string]: RpcPath; }[keyof T & string] : never; type ProtectedRpcPath = Exclude, "health">; process.env.WAKEUP_DRIVER = "memory"; process.env.SANDBOX_PROVIDER = "fake"; process.env.AGENT_RUNTIME = "scripted"; const hasDb = process.env.VERIFY_DATABASE === "1" && Boolean(process.env.DATABASE_URL); const describeWithDatabase = hasDb ? describe : describe.skip; describeWithDatabase("API authorization and resource isolation", () => { let handles: AppHandles; let app: App; const stamp = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const dataDir = mkdtempSync(path.join(tmpdir(), "rakazo-authz-")); beforeAll(async () => { const { createApp } = await import("../../../apps/api/src/app.ts"); handles = await createApp({ databaseUrl: process.env.DATABASE_URL!, dataDir, sandboxProvider: "fake", agentRuntime: "scripted", wakeupDriver: "memory", signupsEnabled: "true", composio: new ComposioEmulator(), }); app = handles.app; }); afterAll(async () => { await handles?.stop(); rmSync(dataDir, { recursive: true, force: true }); }); it("rejects unauthenticated calls to every protected RPC family", async () => { const calls = exhaustiveProtectedCalls([ ["me"], ["profile/get"], ["profile/update", { timeZone: "UTC" }], ["profile/forget", { all: true }], ["deployment/get"], ["deployment/update", { signupsEnabled: true }], ["updater/status"], ["updater/check", {}], ["updater/apply", {}], ["models/list"], ["models/credentials"], ["models/connect", { provider: "test", apiKey: "not-a-real-key" }], ["models/beginOAuth", { provider: "openai-codex" }], ["models/submitOAuthCode", { loginId: "missing-login", code: "fake-code" }], ["models/completeOAuth", { loginId: "missing-login" }], ["models/finishOAuth", { loginId: "missing-login" }], ["models/cancelOAuth", { loginId: "missing-login" }], ["models/setDefault", { provider: "test", modelId: "test/model" }], ["models/probeOpenAiCompatible", { credentialId: "missing-credential" }], ["models/tiers/list"], ["models/tiers/set", { tier: "fast", model: null }], ["spaces/list"], ["spaces/create", { name: "Nope" }], ["spaces/remove", { spaceId: "missing-space" }], ["bots/list"], ["bots/listArchived"], ["bots/get", { botId: "missing-bot" }], ["bots/create", botInput("Unauthenticated")], ["bots/ensureMain"], ["bots/duplicate", { botId: "missing-bot" }], ["bots/update", { botId: "missing-bot", name: "Nope" }], ["bots/builtinTools", { botId: "missing-bot" }], ["bots/archive", { botId: "missing-bot" }], ["bots/restore", { botId: "missing-bot" }], ["bots/remove", { botId: "missing-bot" }], ["groups/create", { name: "Nope", botIds: ["missing-a", "missing-b"] }], ["groups/list"], ["groups/get", { groupId: "missing-group" }], ["groups/update", { groupId: "missing-group", name: "Nope" }], ["groups/remove", { groupId: "missing-group" }], ["botSections/list"], ["botSections/create", { botId: "missing-bot", name: "Planning" }], ["botSections/update", { sectionId: "missing-section", name: "Planning" }], ["threads/get", { botId: "missing-bot" }], ["threads/get", { groupId: "missing-group" }], ["threads/messages", { botId: "missing-bot", before: 1 }], ["threads/subscribe", { botId: "missing-bot", cursor: -1 }], ["threads/send", { botId: "missing-bot", text: "Nope" }], ["threads/send", { groupId: "missing-group", text: "Nope" }], ["threads/stop", { botId: "missing-bot" }], ["threads/clear", { botId: "missing-bot" }], ["threads/followUp", { botId: "missing-bot", text: "Nope" }], [ "threads/answer", { botId: "missing-bot", runId: "missing-run", messageId: "missing-message", answer: "Nope", }, ], ["threads/markRead", { botId: "missing-bot" }], ["threads/markUnread", { botId: "missing-bot" }], ["computer/status", { botId: "missing-bot" }], ["computer/boot", { botId: "missing-bot" }], ["computer/stop", { botId: "missing-bot" }], ["computer/takeover", { botId: "missing-bot" }], ["computer/release", { botId: "missing-bot" }], ["computer/input", { botId: "missing-bot", kind: "key", payload: { key: "A" } }], ["computer/files", { botId: "missing-bot", path: "/" }], ["computer/readFile", { botId: "missing-bot", path: "MEMORY.md" }], ["computer/screenUrl", { botId: "missing-bot" }], ["computer/heartbeat", { botId: "missing-bot" }], ["memory/list", {}], ["memory/update", { documentId: "missing-memory", content: "Nope" }], ["memory/exportMarkdown", {}], ["memory/providerConfig"], [ "memory/connectProvider", { provider: "test", settings: {}, credentials: {}, defaultMemoryScope: "isolated" }, ], ["memory/setDefaultScope", { defaultMemoryScope: "shared" }], ["memory/disconnectProvider"], ["routines/list", { botId: "missing-bot" }], ["routines/create", routineInput("missing-bot")], ["routines/update", { routineId: "missing-routine", name: "Nope" }], ["routines/remove", { routineId: "missing-routine" }], ["routines/testRun", { routineId: "missing-routine" }], ["scratchpad/list", { botId: "missing-bot" }], ["scratchpad/create", { botId: "missing-bot", title: "Nope" }], ["scratchpad/update", { itemId: "missing-item", title: "Nope" }], ["scratchpad/remove", { itemId: "missing-item" }], ["skills/list", { botId: "missing-bot" }], ["skills/get", { skillId: "missing-skill" }], ["skills/start", { botId: "missing-bot", goal: "Demonstrate export" }], [ "skills/appendEvent", { skillId: "missing-skill", event: { at: new Date().toISOString(), kind: "key", key: "a" }, }, ], ["skills/snapshot", { skillId: "missing-skill" }], ["skills/stop", { skillId: "missing-skill" }], ["skills/updateDraft", { skillId: "missing-skill", playbook: skillPlaybookInput() }], ["skills/save", { skillId: "missing-skill" }], ["skills/testRun", { skillId: "missing-skill" }], ["skills/remove", { skillId: "missing-skill" }], ["agentSkills/list"], ["agentSkills/get", { skillId: "missing-skill" }], ["agentSkills/create", { name: "Unauthenticated", description: "Nope", body: "Steps" }], ["agentSkills/update", { skillId: "missing-skill", description: "Nope" }], ["agentSkills/remove", { skillId: "missing-skill" }], ["capabilities/list"], ["capabilities/install", capabilityInput("Unauthenticated")], ["capabilities/remove", { id: "missing-capability" }], ["connections/catalog", {}], ["connections/list"], ["connections/begin", connectionInput("Unauthenticated")], ["connections/complete", { connectionId: "missing-connection" }], ["connections/revoke", { connectionId: "missing-connection" }], ["approvalRules/list"], [ "approvalRules/set", { effect: "require_approval", matchKind: "category", matchValue: "email" }, ], ["approvalRules/remove", { id: "missing-rule" }], ["artifacts/list", { botId: "missing-bot" }], ["usage/list"], ["usage/summary"], ["export/bot", { botId: "missing-bot" }], ["notifications/registerPush", { token: "ExponentPushToken[not-real]" }], ["search/query", { q: "anything" }], ["tasks/list", {}], ["inbox/list"], ["voice/catalog"], ["voice/status"], ["voice/credentials"], ["voice/connect", { provider: "elevenlabs", apiKey: "not-a-real-key" }], ["voice/disconnect", { provider: "elevenlabs" }], ["voice/setVoice", { voiceId: "missing-voice" }], ["voice/voices", {}], ["voice/prepare", { text: "Nope" }], ]); const results = await Promise.all( calls.map(async ([procedure, input]) => ({ procedure, response: await raw(app, "", procedure, input), })), ); for (const { procedure, response } of results) { expect(response.status, procedure).toBe(401); } }); it("prevents one user from reading or mutating another user's resources", async () => { const owner = await signup(app, `owner-authz-${stamp}@rakazo.test`, "Authorization Owner"); const intruder = await signup( app, `intruder-authz-${stamp}@rakazo.test`, "Authorization Intruder", ); const ownerActor = await rpc(app, owner, "me"); const intruderActor = await rpc(app, intruder, "me"); expect(ownerActor.spaceId).not.toBe(intruderActor.spaceId); const ownerBot = await rpc(app, owner, "bots/create", botInput("Owner Bot")); const intruderBot = await rpc(app, intruder, "bots/create", botInput("Intruder Bot")); const ownerRoutine = await rpc<{ id: string }>( app, owner, "routines/create", routineInput(ownerBot.id), ); const ownerScratchpad = await rpc<{ id: string }>(app, owner, "scratchpad/create", { botId: ownerBot.id, title: "Owner open work", notes: "private", }); const ownerSkill = await handles.prisma.taughtSkill.create({ data: { spaceId: ownerActor.spaceId, botId: ownerBot.id, userId: ownerActor.userId, name: "Owner Skill", goal: "Owner-only skill", status: "saved", playbook: skillPlaybookInput(), recording: { events: [], snapshots: [] }, }, }); const ownerAgentSkill = await rpc<{ id: string }>(app, owner, "agentSkills/create", { name: "Owner Recipe", description: "Owner-only shared skill", body: "1. Do the private thing.", }); const ownerCapability = await rpc<{ id: string }>( app, owner, "capabilities/install", capabilityInput("Owner Capability"), ); const ownerConnection = await rpc<{ connectionId: string }>( app, owner, "connections/begin", connectionInput("Owner Connection"), ); const ownerMemory = await handles.prisma.memoryDocument.create({ data: { spaceId: ownerActor.spaceId, userId: ownerActor.userId, botId: ownerBot.id, scope: "bot", path: "PRIVATE.md", content: "owner-only-memory", }, }); const ownerArtifact = await handles.prisma.artifact.create({ data: { spaceId: ownerActor.spaceId, userId: ownerActor.userId, botId: ownerBot.id, name: "owner-secret.txt", mimeType: "text/plain", size: 17, hash: "sha256:not-a-real-hash", storageKey: "test/owner-secret.txt", }, }); const ownerThread = await handles.prisma.thread.findUniqueOrThrow({ where: { botId: ownerBot.id }, }); const ownerTask = await handles.prisma.task.create({ data: { spaceId: ownerActor.spaceId, userId: ownerActor.userId, botId: ownerBot.id, threadId: ownerThread.id, prompt: "owner prompt", status: "waiting_input", }, }); const ownerRun = await handles.prisma.run.create({ data: { spaceId: ownerActor.spaceId, userId: ownerActor.userId, botId: ownerBot.id, threadId: ownerThread.id, taskId: ownerTask.id, status: "waiting_input", trigger: "user", }, }); await createThreadMessage(handles.prisma, { threadId: ownerThread.id, role: "bot", botId: ownerBot.id, runId: ownerRun.id, blocks: [{ kind: "ask", text: "Owner-only question", status: "pending" }], }); const botIdCalls: Array<[string, unknown]> = [ ["bots/get", { botId: ownerBot.id }], ["bots/duplicate", { botId: ownerBot.id }], ["bots/update", { botId: ownerBot.id, name: "Stolen Bot" }], ["bots/builtinTools", { botId: ownerBot.id }], ["bots/archive", { botId: ownerBot.id }], ["bots/restore", { botId: ownerBot.id }], ["threads/get", { botId: ownerBot.id }], ["threads/messages", { botId: ownerBot.id, before: 1 }], ["threads/subscribe", { botId: ownerBot.id, cursor: -1 }], ["threads/send", { botId: ownerBot.id, text: "intruder message" }], ["threads/send", { botId: ownerBot.id, artifactIds: [ownerArtifact.id] }], ["threads/stop", { botId: ownerBot.id }], ["threads/clear", { botId: ownerBot.id }], ["threads/followUp", { botId: ownerBot.id, text: "intruder follow-up" }], [ "threads/answer", { botId: ownerBot.id, runId: ownerRun.id, messageId: "missing-message", answer: "intruder answer", }, ], ["threads/markRead", { botId: ownerBot.id }], ["threads/markUnread", { botId: ownerBot.id }], ["computer/status", { botId: ownerBot.id }], ["computer/boot", { botId: ownerBot.id }], ["computer/stop", { botId: ownerBot.id }], ["computer/takeover", { botId: ownerBot.id }], ["computer/release", { botId: ownerBot.id }], ["computer/input", { botId: ownerBot.id, kind: "key", payload: { key: "A" } }], ["computer/files", { botId: ownerBot.id, path: "/" }], ["computer/readFile", { botId: ownerBot.id, path: "PRIVATE.md" }], ["computer/screenUrl", { botId: ownerBot.id }], ["computer/heartbeat", { botId: ownerBot.id }], ["routines/list", { botId: ownerBot.id }], ["routines/create", routineInput(ownerBot.id)], ["scratchpad/list", { botId: ownerBot.id }], ["scratchpad/create", { botId: ownerBot.id, title: "Stolen item" }], ["skills/list", { botId: ownerBot.id }], ["skills/start", { botId: ownerBot.id, goal: "Intruder demo" }], ["artifacts/list", { botId: ownerBot.id }], [ "artifacts/create", { botId: ownerBot.id, name: "intruder.txt", mimeType: "text/plain", contentBase64: Buffer.from("nope").toString("base64"), }, ], ["artifacts/get", { botId: ownerBot.id, artifactId: ownerArtifact.id }], ["export/bot", { botId: ownerBot.id }], ["voice/prepare", { text: "stolen speech", botId: ownerBot.id }], ]; await Promise.all( botIdCalls.map(([procedure, input]) => expectDenied(app, intruder, procedure, input)), ); const ownerBot2 = await rpc(app, owner, "bots/create", botInput("Owner Bot Two")); const ownerGroup = await rpc<{ id: string }>(app, owner, "groups/create", { name: "Owner Group", botIds: [ownerBot.id, ownerBot2.id], }); const groupCalls: Array<[string, unknown]> = [ ["groups/get", { groupId: ownerGroup.id }], ["groups/update", { groupId: ownerGroup.id, name: "Stolen Group" }], ["groups/remove", { groupId: ownerGroup.id }], ["threads/get", { groupId: ownerGroup.id }], ["threads/messages", { groupId: ownerGroup.id, before: 1 }], ["threads/subscribe", { groupId: ownerGroup.id, cursor: -1 }], ["threads/send", { groupId: ownerGroup.id, text: "intruder group message" }], ["threads/stop", { groupId: ownerGroup.id }], ["threads/followUp", { groupId: ownerGroup.id, text: "intruder follow-up" }], ["threads/markRead", { groupId: ownerGroup.id }], ["threads/markUnread", { groupId: ownerGroup.id }], ]; await Promise.all( groupCalls.map(([procedure, input]) => expectDenied(app, intruder, procedure, input)), ); // The main conversation's own refusals must not reveal it to another user: a foreign main // conversation is answered exactly like a group that does not exist. await rpc(app, owner, "bots/ensureMain"); const ownerMain = (await rpc(app, owner, "spaces/list")).main; expect(ownerMain).not.toBeNull(); for (const [procedure, input] of [ ["groups/archive", {}], ["groups/remove", {}], ["groups/update", { botIds: [intruderBot.id, ownerBot.id] }], ] as const) { const missing = await raw(app, intruder, procedure, { ...input, groupId: "missing-group" }); const foreign = await raw(app, intruder, procedure, { ...input, groupId: ownerMain!.groupId, }); expect(foreign.status, procedure).toBe(missing.status); expect(foreign.status, procedure).not.toBe(400); } expect( (await rpc(app, intruder, "spaces/list")).main, "an intruder has no main conversation of their own yet", ).toBeNull(); // A caller cannot pair their own bot with another workspace's run ID. await expectDenied(app, intruder, "threads/answer", { botId: intruderBot.id, runId: ownerRun.id, messageId: "missing-message", answer: "mixed-resource attack", }); const ownerModel = await rpc(app, owner, "models/connect", { provider: "scripted", apiKey: "fake-owner-scripted-key", }); const ownerTiers = await rpc(app, owner, "models/tiers/set", { tier: "code", model: { credentialId: ownerModel.id, modelId: "scripted" }, }); expect(ownerTiers).toEqual([ { tier: "code", credentialId: ownerModel.id, provider: "scripted", modelId: "scripted" }, ]); const resourceIdCalls = [ [ "models/tiers/set", { tier: "fast", model: { credentialId: ownerModel.id, modelId: "scripted" } }, ], ["models/probeOpenAiCompatible", { credentialId: ownerModel.id }], ["routines/update", { routineId: ownerRoutine.id, name: "Stolen Routine" }], ["routines/remove", { routineId: ownerRoutine.id }], ["routines/testRun", { routineId: ownerRoutine.id }], ["scratchpad/update", { itemId: ownerScratchpad.id, title: "Stolen item" }], ["scratchpad/remove", { itemId: ownerScratchpad.id }], ["skills/get", { skillId: ownerSkill.id }], [ "skills/appendEvent", { skillId: ownerSkill.id, event: { at: new Date().toISOString(), kind: "key", key: "x" } }, ], ["skills/snapshot", { skillId: ownerSkill.id }], ["skills/stop", { skillId: ownerSkill.id }], ["skills/updateDraft", { skillId: ownerSkill.id, playbook: skillPlaybookInput() }], ["skills/save", { skillId: ownerSkill.id }], ["skills/testRun", { skillId: ownerSkill.id }], ["skills/remove", { skillId: ownerSkill.id }], ["agentSkills/get", { skillId: ownerAgentSkill.id }], ["agentSkills/update", { skillId: ownerAgentSkill.id, description: "Stolen" }], ["agentSkills/remove", { skillId: ownerAgentSkill.id }], ["memory/update", { documentId: ownerMemory.id, content: "stolen" }], ["connections/complete", { connectionId: ownerConnection.connectionId }], ] satisfies Array<[string, unknown]>; await Promise.all( resourceIdCalls.map(([procedure, input]) => expectDenied(app, intruder, procedure, input)), ); expect(await rpc(app, intruder, "models/tiers/list")).toEqual([]); expect(await rpc(app, owner, "models/tiers/list")).toEqual(ownerTiers); expect(await rpc(app, intruder, "memory/list", { botId: ownerBot.id })).toEqual([]); expect(await rpc(app, intruder, "memory/exportMarkdown", { botId: ownerBot.id })).toBe( "", ); expect(await rpc>(app, intruder, "capabilities/list")).not.toContainEqual( expect.objectContaining({ id: ownerCapability.id }), ); expect(await rpc>(app, intruder, "agentSkills/list")).not.toContainEqual( expect.objectContaining({ id: ownerAgentSkill.id }), ); expect(await rpc>(app, intruder, "connections/list")).not.toContainEqual( expect.objectContaining({ id: ownerConnection.connectionId }), ); expect( await rpc<{ hits: unknown[] }>(app, intruder, "search/query", { q: ownerBot.name }), ).toEqual({ hits: [] }); const ownerDelegatedTask = await handles.prisma.delegatedTask.create({ data: { userId: ownerActor.userId, spaceId: ownerActor.spaceId, threadId: ownerThread.id, requesterBotId: ownerBot.id, assigneeBotId: ownerBot.id, assigneeSpaceId: ownerActor.spaceId, title: "Owner task", brief: "owner-only brief", }, }); expect( await rpc>(app, owner, "tasks/list", { threadId: ownerThread.id }), ).toEqual([expect.objectContaining({ id: ownerDelegatedTask.id })]); expect(await rpc(app, intruder, "tasks/list", {})).toEqual([]); expect(await rpc(app, intruder, "tasks/list", { threadId: ownerThread.id })).toEqual( [], ); expect( (await rpc<{ items: Array<{ runId?: string }> }>(app, owner, "inbox/list")).items, ).toContainEqual(expect.objectContaining({ kind: "ask", runId: ownerRun.id })); const intruderInbox = JSON.stringify(await rpc(app, intruder, "inbox/list")); expect(intruderInbox).not.toContain(ownerBot.id); expect(intruderInbox).not.toContain("Owner-only question"); await expectDenied(app, intruder, "inbox/list", {}, ownerActor.spaceId); // These endpoints are deliberately idempotent for unknown IDs. Success must not mutate // a row in a different workspace or disclose whether it exists. await rpc(app, intruder, "capabilities/remove", { id: ownerCapability.id }); await rpc(app, intruder, "connections/revoke", { connectionId: ownerConnection.connectionId, }); expect( await handles.prisma.capabilityInstall.findUnique({ where: { id: ownerCapability.id } }), ).not.toBeNull(); expect( await handles.prisma.connection.findUniqueOrThrow({ where: { id: ownerConnection.connectionId }, }), ).toMatchObject({ status: "connected", userId: ownerActor.userId }); const ownerBotAfter = await handles.prisma.bot.findUniqueOrThrow({ where: { id: ownerBot.id }, }); expect(ownerBotAfter.name).toBe("Owner Bot"); expect( await handles.prisma.routine.findUniqueOrThrow({ where: { id: ownerRoutine.id } }), ).toMatchObject({ name: "Owner Routine" }); expect( await handles.prisma.scratchpadItem.findUniqueOrThrow({ where: { id: ownerScratchpad.id } }), ).toMatchObject({ title: "Owner open work", notes: "private" }); expect( await handles.prisma.memoryDocument.findUniqueOrThrow({ where: { id: ownerMemory.id } }), ).toMatchObject({ content: "owner-only-memory" }); // The user profile is per user: another user's fact ids and spaces are out of reach. const ownerProfile = await rpc<{ facts: Array<{ id: string; status: string }> }>( app, owner, "profile/update", { addFacts: [ { category: "work", value: "Owner works nights" }, { category: "health", value: "Owner health note", source: "onboarding" }, ], }, ); const ownerFactIds = ownerProfile.facts.map((fact) => fact.id); expect(ownerProfile.facts.map((fact) => fact.status).sort()).toEqual(["active", "pending"]); const profileCalls = [ ["profile/update", { confirmFactIds: ownerFactIds }], ["profile/update", { editFacts: [{ id: ownerFactIds[0], value: "Stolen" }] }], [ "profile/update", { addFacts: [{ category: "work", value: "Planted fact", spaceIds: [ownerActor.spaceId] }], }, ], ] satisfies Array<[string, unknown]>; await Promise.all( profileCalls.map(([procedure, input]) => expectDenied(app, intruder, procedure, input)), ); expect( await rpc<{ facts: unknown[] }>(app, intruder, "profile/forget", { factIds: ownerFactIds }), ).toMatchObject({ facts: [] }); expect(await rpc<{ facts: unknown[] }>(app, intruder, "profile/get")).toMatchObject({ facts: [], }); expect( await handles.prisma.userFact.findMany({ where: { id: { in: ownerFactIds } }, select: { userId: true, value: true, status: true }, orderBy: { value: "asc" }, }), ).toEqual([ { userId: ownerActor.userId, value: "Owner health note", status: "pending" }, { userId: ownerActor.userId, value: "Owner works nights", status: "active" }, ]); // Destructive bot removal is checked last so an authorization regression is unmistakable. await expectDenied(app, intruder, "bots/remove", { botId: ownerBot.id, deleteMemories: true, }); expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.toBeNull(); }); it("joins the organization default space once", async () => { const owner = await signup( app, `default-join-owner-${stamp}@rakazo.test`, "Default Join Owner", ); const member = await signup( app, `default-join-member-${stamp}@rakazo.test`, "Default Join Member", ); const ownerActor = await rpc(app, owner, "me"); const memberActor = await rpc(app, member, "me"); const space = await handles.prisma.space.findUniqueOrThrow({ where: { id: ownerActor.spaceId }, select: { organizationId: true, isDefault: true }, }); expect(space.isDefault).toBe(true); const joinId = `default-join-${stamp}`; await joinMemberOnSpace(handles.prisma, { id: joinId, organizationId: space.organizationId, spaceId: ownerActor.spaceId, userId: memberActor.userId, }); const joined = await handles.prisma.spaceMember.findMany({ where: { spaceId: ownerActor.spaceId, userId: memberActor.userId }, }); expect(joined).toHaveLength(1); expect(joined[0]?.role).toBe("member"); await joinMemberOnSpace(handles.prisma, { id: `${joinId}-again`, organizationId: space.organizationId, spaceId: ownerActor.spaceId, userId: memberActor.userId, }); const rejoined = await handles.prisma.spaceMember.findMany({ where: { spaceId: ownerActor.spaceId, userId: memberActor.userId }, }); expect(rejoined).toHaveLength(1); expect(rejoined[0]?.role).toBe("member"); }); it("keeps each shared-space member's inbox to their own bots", async () => { const owner = await signup(app, `inbox-owner-${stamp}@rakazo.test`, "Inbox Owner"); const member = await signup(app, `inbox-member-${stamp}@rakazo.test`, "Inbox Member"); const ownerActor = await rpc(app, owner, "me"); const memberActor = await rpc(app, member, "me"); const sharedSpaceId = ownerActor.spaceId; const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: sharedSpaceId }, select: { organizationId: true }, }); await joinMemberOnSpace(handles.prisma, { id: `inbox-org-member-${stamp}`, organizationId: sharedRow.organizationId, spaceId: sharedSpaceId, userId: memberActor.userId, }); const ownerBot = await rpc( app, owner, "bots/create", botInput("Owner Inbox Bot"), sharedSpaceId, ); const thread = await handles.prisma.thread.findUniqueOrThrow({ where: { botId: ownerBot.id }, }); const task = await handles.prisma.task.create({ data: { spaceId: sharedSpaceId, userId: ownerActor.userId, botId: ownerBot.id, threadId: thread.id, prompt: "owner prompt", status: "waiting_input", }, }); const run = await handles.prisma.run.create({ data: { spaceId: sharedSpaceId, userId: ownerActor.userId, botId: ownerBot.id, threadId: thread.id, taskId: task.id, status: "waiting_input", trigger: "user", }, }); await createThreadMessage(handles.prisma, { threadId: thread.id, role: "bot", botId: ownerBot.id, runId: run.id, blocks: [{ kind: "ask", text: "Owner-only question", status: "pending" }], }); expect( (await rpc<{ items: Array<{ runId?: string }> }>(app, owner, "inbox/list", {}, sharedSpaceId)) .items, ).toContainEqual(expect.objectContaining({ kind: "ask", runId: run.id })); expect(await rpc(app, member, "inbox/list", {}, sharedSpaceId)).toEqual({ items: [] }); }); it("prevents one shared-space member from hijacking another member's Team Computer takeover", async () => { const owner = await signup(app, `takeover-owner-${stamp}@rakazo.test`, "Takeover Owner"); const member = await signup(app, `takeover-member-${stamp}@rakazo.test`, "Takeover Member"); const ownerActor = await rpc(app, owner, "me"); const memberActor = await rpc(app, member, "me"); const sharedSpaceId = ownerActor.spaceId; const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: sharedSpaceId }, select: { organizationId: true }, }); await joinMemberOnSpace(handles.prisma, { id: `takeover-org-member-${stamp}`, organizationId: sharedRow.organizationId, spaceId: sharedSpaceId, userId: memberActor.userId, }); const ownerBot = await rpc( app, owner, "bots/create", botInput("Owner Bot"), sharedSpaceId, ); const memberBot = await rpc( app, member, "bots/create", botInput("Member Bot"), sharedSpaceId, ); const ownerBotRow = await handles.prisma.bot.findUniqueOrThrow({ where: { id: ownerBot.id } }); const memberBotRow = await handles.prisma.bot.findUniqueOrThrow({ where: { id: memberBot.id }, }); expect(ownerBotRow.computerId).toBe(memberBotRow.computerId); await rpc(app, owner, "computer/boot", { botId: ownerBot.id }, sharedSpaceId); await rpc(app, owner, "computer/takeover", { botId: ownerBot.id }, sharedSpaceId); const memberTakeover = await raw( app, member, "computer/takeover", { botId: memberBot.id }, sharedSpaceId, ); expect(memberTakeover.status).toBe(409); expect(await memberTakeover.text()).toMatch(/Another member holds control/i); const computer = await handles.prisma.computer.findUniqueOrThrow({ where: { id: ownerBotRow.computerId! }, select: { controlBotId: true }, }); expect(computer.controlBotId).toBe(ownerBot.id); }); it("prevents one shared-space member from deleting another member's routine", async () => { const owner = await signup(app, `routine-owner-${stamp}@rakazo.test`, "Routine Owner"); const member = await signup(app, `routine-member-${stamp}@rakazo.test`, "Routine Member"); const memberActor = await rpc(app, member, "me"); const shared = await rpc(app, owner, "spaces/create", { name: "Shared routines" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `routine-member-org-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `routine-member-space-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerBot = await rpc(app, owner, "bots/create", botInput("Owner Bot"), shared.id); const ownerRoutine = await rpc<{ id: string }>( app, owner, "routines/create", routineInput(ownerBot.id), shared.id, ); await expectDenied(app, member, "routines/remove", { routineId: ownerRoutine.id }, shared.id); expect( await handles.prisma.routine.findUnique({ where: { id: ownerRoutine.id } }), ).not.toBeNull(); }); it("keeps Team Computer bot homes private to each member in a shared Space", async () => { const owner = await signup(app, `team-home-owner-${stamp}@rakazo.test`, "Team Home Owner"); const member = await signup(app, `team-home-member-${stamp}@rakazo.test`, "Team Home Member"); const shared = await rpc(app, owner, "spaces/create", { name: "Shared Team Computer" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); const memberActor = await rpc(app, member, "me"); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `team-home-org-member-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `team-home-space-member-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerBot = await rpc( app, owner, "bots/create", botInput("Owner Team Bot"), shared.id, ); const ownerBotTwo = await rpc( app, owner, "bots/create", botInput("Owner Team Bot Two"), shared.id, ); const memberBot = await rpc( app, member, "bots/create", botInput("Member Team Bot"), shared.id, ); const secret = "owner-only-workspace-secret"; const ownerNotesDir = path.join( dataDir, "homes", `team-${shared.id}`, "bots", ownerBot.id, "notes", ); mkdirSync(ownerNotesDir, { recursive: true }); writeFileSync(path.join(ownerNotesDir, "secret.txt"), secret); await expectForbidden( app, member, "computer/readFile", { botId: memberBot.id, path: `bots/${ownerBot.id}/notes/secret.txt` }, shared.id, ); const memberNotesDir = path.join( dataDir, "homes", `team-${shared.id}`, "bots", memberBot.id, "notes", ); mkdirSync(memberNotesDir, { recursive: true }); writeFileSync(path.join(memberNotesDir, "mine.txt"), "member-only"); const botListing = await rpc>( app, member, "computer/files", { botId: memberBot.id, path: "bots" }, shared.id, ); expect(botListing.map((entry) => entry.path)).toEqual([`bots/${memberBot.id}`]); const ownerCrossBot = await rpc<{ content: string }>( app, owner, "computer/readFile", { botId: ownerBotTwo.id, path: `bots/${ownerBot.id}/notes/secret.txt` }, shared.id, ); expect(ownerCrossBot.content).toBe(secret); }); it("blocks Team Computer takeover on another bot while teaching is recording", async () => { const cookie = await signup(app, `teach-takeover-${stamp}@rakazo.test`, "Teach Takeover"); const teacher = await rpc(app, cookie, "bots/create", botInput("Teacher")); const sibling = await rpc(app, cookie, "bots/create", botInput("Sibling")); const [teacherRow, siblingRow] = await Promise.all([ handles.prisma.bot.findUniqueOrThrow({ where: { id: teacher.id } }), handles.prisma.bot.findUniqueOrThrow({ where: { id: sibling.id } }), ]); expect(teacherRow.computerId).toBe(siblingRow.computerId); await rpc(app, cookie, "computer/boot", { botId: teacher.id }); const skill = await rpc<{ id: string; status: string }>(app, cookie, "skills/start", { botId: teacher.id, goal: "Demonstrate export", }); expect(skill.status).toBe("recording"); const blockedTakeover = await raw(app, cookie, "computer/takeover", { botId: sibling.id }); expect(blockedTakeover.status).toBe(409); expect(await blockedTakeover.text()).toMatch(/Stop teaching first/i); await rpc(app, cookie, "computer/input", { botId: teacher.id, kind: "key", payload: { key: "x" }, }); const stopped = await rpc<{ status: string }>(app, cookie, "skills/stop", { skillId: skill.id, }); expect(stopped.status).toBe("draft"); const computer = await handles.prisma.computer.findUniqueOrThrow({ where: { id: teacherRow.computerId! }, select: { controlHolder: true, controlBotId: true }, }); expect(computer.controlHolder).toBe("bot"); expect(computer.controlBotId).toBeNull(); }); it("scopes Team Computer export to the exporting member's bot workspace in a shared Space", async () => { const owner = await signup(app, `export-owner-${stamp}@rakazo.test`, "Export Owner"); const member = await signup(app, `export-member-${stamp}@rakazo.test`, "Export Member"); const ownerActor = await rpc(app, owner, "me"); const memberActor = await rpc(app, member, "me"); const shared = await rpc(app, owner, "spaces/create", { name: "Shared export" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `export-member-org-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `export-member-space-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerBot = await rpc(app, owner, "bots/create", botInput("Owner Bot"), shared.id); const memberBot = await rpc(app, member, "bots/create", botInput("Member Bot"), shared.id); const home = new LocalAgentHomeStore(dataDir); const homeKey = computerHomeKey("team", shared.id); const homeContext = { operationId: "test", traceId: "test", spaceId: shared.id, userId: ownerActor.userId, signal: new AbortController().signal, }; await home.writeFile( homeKey, `${teamBotWorkspaceDirectory(ownerBot.id)}/PRIVATE.md`, "owner-workspace-secret", homeContext, ); await home.writeFile( homeKey, `${teamBotWorkspaceDirectory(memberBot.id)}/notes.txt`, "member-notes", homeContext, ); const manifest = await rpc<{ files: Array<{ path: string; content: string }> }>( app, member, "export/bot", { botId: memberBot.id }, shared.id, ); expect(manifest.files.some((file) => file.content.includes("owner-workspace-secret"))).toBe( false, ); expect(manifest.files.some((file) => file.content.includes("member-notes"))).toBe(true); }); it("keeps approval rules private to each user in a shared Space", async () => { const owner = await signup(app, `approval-owner-${stamp}@rakazo.test`, "Approval Owner"); const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member"); const ownerActor = await rpc(app, owner, "me"); const memberActor = await rpc(app, member, "me"); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `approval-member-${stamp}`, organizationId: ownerActor.spaceId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerRule = await rpc<{ id: string }>(app, owner, "approvalRules/set", { effect: "always_allow", matchKind: "tool", matchValue: "destination.write", }); expect(await rpc(app, member, "approvalRules/list")).toEqual([]); const memberRule = await rpc<{ id: string }>(app, member, "approvalRules/set", { effect: "require_approval", matchKind: "category", matchValue: "email", }); expect(await rpc>(app, owner, "approvalRules/list")).toEqual([ expect.objectContaining({ id: ownerRule.id }), ]); expect(await rpc>(app, member, "approvalRules/list")).toEqual([ expect.objectContaining({ id: memberRule.id }), ]); await rpc(app, member, "approvalRules/remove", { id: ownerRule.id }); expect( await handles.prisma.actionApprovalRule.findUnique({ where: { id: ownerRule.id } }), ).not.toBeNull(); }); it("redacts memory provider details in aiConsent/status for non-owner members", async () => { const owner = await signup(app, `ai-consent-owner-${stamp}@rakazo.test`, "Consent Owner"); const ownerActor = await rpc(app, owner, "me"); const shared = await rpc(app, owner, "spaces/create", { name: "Consent shared" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); const secret = await handles.prisma.secret.create({ data: { userId: ownerActor.userId, spaceId: shared.id, kind: "memory-provider", ciphertext: "encrypted-test-key", }, }); await handles.prisma.spaceMemoryConfig.create({ data: { spaceId: shared.id, userId: ownerActor.userId, provider: "supermemory", settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, secretId: secret.id, defaultMemoryScope: "isolated", }, }); const member = await signup(app, `ai-consent-member-${stamp}@rakazo.test`, "Consent Member"); const memberActor = await rpc(app, member, "me"); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `ai-consent-org-member-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `ai-consent-space-member-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerStatus = await rpc( app, owner, "aiConsent/status", { uses: ["memory"] }, shared.id, ); const memberStatus = await rpc( app, member, "aiConsent/status", { uses: ["memory"] }, shared.id, ); const ownerMemory = ownerStatus.recipients.find((recipient) => recipient.use === "memory"); const memberMemory = memberStatus.recipients.find((recipient) => recipient.use === "memory"); expect(ownerMemory?.name).toContain("127.0.0.1:8123"); expect(memberMemory?.name).toBe("Space memory"); expect(JSON.stringify(memberStatus.recipients)).not.toContain("127.0.0.1"); expect(JSON.stringify(memberStatus.recipients)).not.toMatch(/supermemory/i); }); it("keeps space data and computers behind the selected space boundary", async () => { const cookie = await signup(app, `spaces-${stamp}@rakazo.test`, "Space Owner"); const original = await rpc(app, cookie, "me"); const originalBot = await rpc(app, cookie, "bots/create", botInput("Open source")); const support = await rpc(app, cookie, "spaces/create", { name: "Customer support", }); const storedSpaces = await handles.prisma.space.findMany({ where: { id: { in: [original.spaceId, support.id] } }, select: { id: true, organizationId: true }, }); expect(new Set(storedSpaces.map((space) => space.organizationId))).toEqual( new Set([original.spaceId]), ); const otherOrganizationId = `spaces-other-org-${stamp}`; const otherWorkspaceId = `spaces-other-workspace-${stamp}`; await handles.prisma.$transaction([ handles.prisma.organization.create({ data: { id: otherOrganizationId, name: "Other company", slug: otherOrganizationId, createdAt: new Date(), }, }), handles.prisma.member.create({ data: { id: `spaces-other-member-${stamp}`, organizationId: otherOrganizationId, userId: original.userId, role: "owner", createdAt: new Date(), }, }), handles.prisma.space.create({ data: { id: otherWorkspaceId, organizationId: otherOrganizationId, name: "Other company space", }, }), handles.prisma.spaceMember.create({ data: { id: `spaces-other-space-member-${stamp}`, spaceId: otherWorkspaceId, organizationId: otherOrganizationId, userId: original.userId, createdAt: new Date(), }, }), ]); const supportMe = await rpc(app, cookie, "me", {}, support.id); expect(supportMe.spaceId).toBe(support.id); const supportBot = await rpc(app, cookie, "bots/create", botInput("Support"), support.id); expect(await rpc>(app, cookie, "bots/list")).toEqual([ expect.objectContaining({ id: originalBot.id }), ]); expect(await rpc>(app, cookie, "bots/list", {}, support.id)).toEqual([ expect.objectContaining({ id: supportBot.id }), ]); await expectDenied(app, cookie, "bots/get", { botId: supportBot.id }); await expectDenied(app, cookie, "bots/get", { botId: originalBot.id }, support.id); const navigation = await rpc(app, cookie, "spaces/list"); expect(navigation.current).toEqual( expect.objectContaining({ id: original.spaceId, bots: [expect.objectContaining({ id: originalBot.id })], }), ); expect(navigation.spaces).toEqual( expect.arrayContaining([ expect.objectContaining({ id: original.spaceId, bots: [expect.objectContaining({ id: originalBot.id, notifyOnFinish: false })], }), expect.objectContaining({ id: support.id, name: "Customer support", bots: [expect.objectContaining({ id: supportBot.id, notifyOnFinish: false })], }), ]), ); expect(navigation.spaces).not.toEqual( expect.arrayContaining([expect.objectContaining({ id: otherWorkspaceId })]), ); const supportNavigation = await rpc( app, cookie, "spaces/list", {}, support.id, ); expect(supportNavigation.current).toEqual( expect.objectContaining({ id: support.id, bots: [expect.objectContaining({ id: supportBot.id })], }), ); const otherOrganizationNavigation = await rpc( app, cookie, "spaces/list", {}, otherWorkspaceId, ); expect(otherOrganizationNavigation.spaces.map((space) => space.id)).toEqual([otherWorkspaceId]); const storedBots = await handles.prisma.bot.findMany({ where: { id: { in: [originalBot.id, supportBot.id] } }, select: { id: true, spaceId: true, computerId: true }, }); const storedOriginal = storedBots.find((bot) => bot.id === originalBot.id); const storedSupport = storedBots.find((bot) => bot.id === supportBot.id); expect(storedOriginal?.spaceId).toBe(original.spaceId); expect(storedSupport?.spaceId).toBe(support.id); expect(storedOriginal?.computerId).not.toBe(storedSupport?.computerId); const intruder = await signup(app, `spaces-intruder-${stamp}@rakazo.test`, "Intruder"); await expectDenied(app, intruder, "bots/list", {}, support.id); }); it("restricts memory provider configuration to Space owners", async () => { const owner = await signup(app, `memory-provider-owner-${stamp}@rakazo.test`, "Memory Owner"); const member = await signup( app, `memory-provider-member-${stamp}@rakazo.test`, "Memory Member", ); const memberActor = await rpc(app, member, "me"); const shared = await rpc(app, owner, "spaces/create", { name: "Shared memory config" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `memory-provider-member-org-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `memory-provider-member-space-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); const ownerActor = await rpc(app, owner, "me", {}, shared.id); const secret = await handles.prisma.secret.create({ data: { userId: ownerActor.userId, spaceId: shared.id, kind: "memory-provider", ciphertext: "encrypted-memory-provider-key", }, }); await handles.prisma.spaceMemoryConfig.create({ data: { spaceId: shared.id, userId: ownerActor.userId, provider: "supermemory", settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, secretId: secret.id, defaultMemoryScope: "isolated", }, }); await expectForbidden(app, member, "memory/providerConfig", {}, shared.id); await expectForbidden( app, member, "memory/connectProvider", { provider: "supermemory", settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, credentials: { apiKey: "not-a-real-key" }, defaultMemoryScope: "isolated", }, shared.id, ); await expectForbidden( app, member, "memory/setDefaultScope", { defaultMemoryScope: "shared" }, shared.id, ); await expectForbidden(app, member, "memory/disconnectProvider", {}, shared.id); expect(await rpc(app, owner, "memory/providerConfig", {}, shared.id)).toEqual({ provider: "supermemory", settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, defaultMemoryScope: "isolated", updatedAt: expect.any(String), }); }); it("enforces the space limit across concurrent creation requests", async () => { const cookie = await signup(app, `space-limit-${stamp}@rakazo.test`, "Space Limit"); const actor = await rpc(app, cookie, "me"); const currentSpace = await handles.prisma.space.findUniqueOrThrow({ where: { id: actor.spaceId }, select: { organizationId: true }, }); const extraSpaces = Array.from({ length: 30 }, (_, index) => ({ id: `limit-space-${stamp}-${index}`, organizationId: currentSpace.organizationId, name: `Limit space ${index}`, createdAt: new Date(), })); await handles.prisma.space.createMany({ data: extraSpaces }); await handles.prisma.spaceMember.createMany({ data: extraSpaces.map((space, index) => ({ id: `limit-member-${stamp}-${index}`, spaceId: space.id, organizationId: space.organizationId, userId: actor.userId, createdAt: space.createdAt, })), }); const results = await Promise.allSettled([ rpc(app, cookie, "spaces/create", { name: "Concurrent A" }), rpc(app, cookie, "spaces/create", { name: "Concurrent B" }), ]); expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); expect(results.filter((result) => result.status === "rejected")).toHaveLength(1); await expect( handles.prisma.spaceMember.count({ where: { userId: actor.userId, organizationId: currentSpace.organizationId }, }), ).resolves.toBe(32); }); it("maps model tiers per Space and carries them into a new Space", async () => { const cookie = await signup(app, `model-tiers-${stamp}@rakazo.test`, "Model Tiers"); const model = await rpc(app, cookie, "models/connect", { provider: "scripted", apiKey: "fake-tier-scripted-key", }); const mapped = [ { tier: "fast", credentialId: model.id, provider: "scripted", modelId: "scripted" }, ]; await expect( rpc(app, cookie, "models/tiers/set", { tier: "fast", model: { credentialId: model.id, modelId: "scripted" }, }), ).resolves.toEqual(mapped); // Catalog providers only run catalog models; free-form ids belong to OpenAI-compatible servers. const unknownModel = await raw(app, cookie, "models/tiers/set", { tier: "code", model: { credentialId: model.id, modelId: "not-a-catalog-model" }, }); expect(unknownModel.status).toBe(400); const created = await rpc(app, cookie, "spaces/create", { name: "Tier copy" }); await expect(rpc(app, cookie, "models/tiers/list", {}, created.id)).resolves.toEqual(mapped); await rpc(app, cookie, "models/tiers/set", { tier: "fast", model: null }, created.id); await expect(rpc(app, cookie, "models/tiers/list", {}, created.id)).resolves.toEqual([]); await expect(rpc(app, cookie, "models/tiers/list")).resolves.toEqual(mapped); const bot = await rpc<{ id: string; modelTier: string | null }>( app, cookie, "bots/create", botInput("Tiered"), ); await expect( rpc(app, cookie, "bots/update", { botId: bot.id, modelTier: "fast" }), ).resolves.toMatchObject({ id: bot.id, modelTier: "fast" }); await expect(rpc(app, cookie, "bots/duplicate", { botId: bot.id })).resolves.toMatchObject({ modelTier: "fast", }); // A pinned model and a tier replace each other, so a client that only knows models // (mobile) never leaves a tier silently overriding the model it picked. await expect( rpc(app, cookie, "bots/update", { botId: bot.id, modelProvider: "scripted", modelId: "scripted", }), ).resolves.toMatchObject({ modelProvider: "scripted", modelId: "scripted", modelTier: null }); await expect( rpc(app, cookie, "bots/update", { botId: bot.id, modelTier: "code" }), ).resolves.toMatchObject({ modelProvider: null, modelId: null, modelTier: "code" }); await expect( rpc(app, cookie, "bots/update", { botId: bot.id, modelTier: null }), ).resolves.toMatchObject({ id: bot.id, modelTier: null }); }); it("reuses provider credentials and copies their selections into a new Space", async () => { const cookie = await signup(app, `space-provider-copy-${stamp}@rakazo.test`, "Provider Copy"); const actor = await rpc(app, cookie, "me"); const model = await rpc(app, cookie, "models/connect", { provider: "copy-provider", apiKey: "fake-copy-model-key", label: "Copy provider", modelId: "copy/model", }); const voice = await rpc<{ id: string; voiceId: string }>(app, cookie, "voice/connect", { provider: "scripted", apiKey: "fake-copy-voice-key", }); const created = await rpc(app, cookie, "spaces/create", { name: "Copied defaults" }); const [modelPreference, voicePreference] = await Promise.all([ handles.prisma.spaceModelPreference.findUnique({ where: { spaceId_userId_credentialId: { spaceId: created.id, userId: actor.userId, credentialId: model.id, }, }, }), handles.prisma.spaceVoicePreference.findUnique({ where: { spaceId_userId_credentialId: { spaceId: created.id, userId: actor.userId, credentialId: voice.id, }, }, }), ]); expect(modelPreference).toMatchObject({ modelId: "copy/model", isDefault: true }); expect(voicePreference).toMatchObject({ voiceId: voice.voiceId, isDefault: true }); await rpc( app, cookie, "voice/setVoice", { provider: "scripted", voiceId: "space-specific-voice" }, created.id, ); await expect( handles.prisma.spaceVoicePreference.findUniqueOrThrow({ where: { spaceId_userId_credentialId: { spaceId: actor.spaceId, userId: actor.userId, credentialId: voice.id, }, }, }), ).resolves.toMatchObject({ voiceId: voice.voiceId, isDefault: true }); await expect( handles.prisma.spaceVoicePreference.findUniqueOrThrow({ where: { spaceId_userId_credentialId: { spaceId: created.id, userId: actor.userId, credentialId: voice.id, }, }, }), ).resolves.toMatchObject({ voiceId: "space-specific-voice", isDefault: true }); await expect( handles.prisma.userModelCredential.count({ where: { userId: actor.userId } }), ).resolves.toBe(1); await expect( handles.prisma.userVoiceCredential.count({ where: { userId: actor.userId } }), ).resolves.toBe(1); }); it("shares model credentials while keeping defaults private to each space", async () => { const cookie = await signup(app, `model-defaults-${stamp}@rakazo.test`, "Model Defaults"); const actor = await rpc(app, cookie, "me"); const support = await rpc(app, cookie, "spaces/create", { name: "Support models" }); const expectSpaceModelDefault = async (spaceId: string, provider: string, modelId: string) => { const preference = await handles.prisma.spaceModelPreference.findFirst({ where: { userId: actor.userId, spaceId, isDefault: true }, include: { credential: true }, }); expect(preference).toMatchObject({ modelId, credential: { provider } }); }; const connectedA = await rpc(app, cookie, "models/connect", { provider: "provider-a", apiKey: "fake-provider-a-key", label: "Provider A", modelId: "a/one", }); expect(connectedA.isDefault).toBe(true); const providerABeforeRotation = await handles.prisma.userModelCredential.findUniqueOrThrow({ where: { id: connectedA.id }, }); const rotatedA = await rpc(app, cookie, "models/connect", { provider: "provider-a", apiKey: "fake-provider-a-replacement-key", label: "Provider A rotated", modelId: "a/rotated", }); const providerAAfterRotation = await handles.prisma.userModelCredential.findUniqueOrThrow({ where: { id: connectedA.id }, }); expect(rotatedA.id).toBe(connectedA.id); expect(providerAAfterRotation.secretId).not.toBe(providerABeforeRotation.secretId); expect( await handles.prisma.secret.findUnique({ where: { id: providerABeforeRotation.secretId } }), ).toBeNull(); await expect( handles.prisma.secret.findUniqueOrThrow({ where: { id: providerAAfterRotation.secretId } }), ).resolves.toMatchObject({ userId: actor.userId, spaceId: null, kind: "model" }); expect( await handles.prisma.userModelCredential.count({ where: { userId: actor.userId, provider: "provider-a" }, }), ).toBe(1); const supportCredentials = await rpc( app, cookie, "models/credentials", {}, support.id, ); expect(supportCredentials).toEqual( expect.arrayContaining([ expect.objectContaining({ id: connectedA.id, provider: "provider-a", isDefault: false }), ]), ); await rpc( app, cookie, "models/setDefault", { provider: "provider-a", modelId: "a/support" }, support.id, ); await expectSpaceModelDefault(support.id, "provider-a", "a/support"); await expectSpaceModelDefault(actor.spaceId, "provider-a", "a/rotated"); const connectedB = await rpc(app, cookie, "models/connect", { provider: "provider-b", apiKey: "fake-provider-b-key", label: "Provider B", modelId: "b/one", }); expect(connectedB.isDefault).toBe(true); await expectSpaceModelDefault(actor.spaceId, "provider-b", "b/one"); await rpc(app, cookie, "models/setDefault", { provider: "provider-a", modelId: "a/two" }); await expectSpaceModelDefault(actor.spaceId, "provider-a", "a/two"); await rpc(app, cookie, "models/setDefault", { provider: "provider-b", modelId: "b/two" }); await expectSpaceModelDefault(actor.spaceId, "provider-b", "b/two"); await rpc(app, cookie, "models/setDefault", { provider: "provider-a", modelId: "a/three" }); await expectSpaceModelDefault(actor.spaceId, "provider-a", "a/three"); await expectSpaceModelDefault(support.id, "provider-a", "a/support"); const listed = await rpc(app, cookie, "models/credentials"); expect(JSON.stringify(listed)).not.toContain("fake-provider-a-key"); expect(JSON.stringify(listed)).not.toContain("fake-provider-b-key"); const missing = await raw(app, cookie, "models/setDefault", { provider: "missing-provider", modelId: "missing/model", }); expect(missing.status).toBeGreaterThanOrEqual(400); expect(await missing.text()).toMatch(/credential/i); }); it("deletes only empty, non-default spaces", async () => { const cookie = await signup(app, `space-delete-${stamp}@rakazo.test`, "Space Delete"); const actor = await rpc(app, cookie, "me"); const empty = await rpc(app, cookie, "spaces/create", { name: "Temporary" }); const busy = await rpc(app, cookie, "spaces/create", { name: "Busy" }); const busyBot = await rpc(app, cookie, "bots/create", botInput("Busy bot"), busy.id); await expect(raw(app, cookie, "spaces/remove", { spaceId: busy.id })).resolves.toMatchObject({ status: 400, }); await expect( handles.prisma.space.findUnique({ where: { id: busy.id } }), ).resolves.not.toBeNull(); await rpc(app, cookie, "bots/archive", { botId: busyBot.id }, busy.id); expect( (await rpc(app, cookie, "spaces/list", {}, busy.id)).spaces.find( (space) => space.id === busy.id, ), ).toMatchObject({ hasContent: true, canDelete: false }); await expect(raw(app, cookie, "spaces/remove", { spaceId: busy.id })).resolves.toMatchObject({ status: 400, }); await expect( raw(app, cookie, "spaces/remove", { spaceId: actor.spaceId }), ).resolves.toMatchObject({ status: 400 }); await expect( raw(app, cookie, "spaces/remove", { spaceId: "missing-space" }), ).resolves.toMatchObject({ status: 404 }); const removed = await rpc<{ ok: true; activeSpaceId: string }>(app, cookie, "spaces/remove", { spaceId: empty.id, }); expect(removed).toEqual({ ok: true, activeSpaceId: actor.spaceId }); await expect(handles.prisma.space.findUnique({ where: { id: empty.id } })).resolves.toBeNull(); const navigation = await rpc(app, cookie, "spaces/list"); expect(navigation.spaces.map((space) => space.id)).not.toContain(empty.id); const intruder = await signup(app, `space-delete-intruder-${stamp}@rakazo.test`, "Intruder"); await expect(raw(app, intruder, "spaces/remove", { spaceId: busy.id })).resolves.toMatchObject({ status: 404, }); // Shared-space members must not delete; only the SpaceMember owner may. const shared = await rpc(app, cookie, "spaces/create", { name: "Shared empty" }); expect(shared.canDelete).toBe(true); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); const memberCookie = await signup( app, `space-delete-member-${stamp}@rakazo.test`, "Space Member", ); const memberActor = await rpc(app, memberCookie, "me"); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { id: `space-delete-org-member-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await handles.prisma.spaceMember.create({ data: { id: `space-delete-space-member-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", createdAt: new Date(), }, }); await expect( raw(app, memberCookie, "spaces/remove", { spaceId: shared.id }, shared.id), ).resolves.toMatchObject({ status: 403 }); await expect( handles.prisma.space.findUnique({ where: { id: shared.id } }), ).resolves.not.toBeNull(); // Another member's bot still counts as content for onboarding emptiness. const ownerBot = await rpc( app, cookie, "bots/create", botInput("Owner shared bot"), shared.id, ); const ownerNavigation = await rpc(app, cookie, "spaces/list", {}, shared.id); expect(ownerNavigation.spaces).toEqual( expect.arrayContaining([ expect.objectContaining({ id: shared.id, hasContent: true, canDelete: false }), ]), ); const memberNavigation = await rpc( app, memberCookie, "spaces/list", {}, shared.id, ); expect(memberNavigation.spaces).toEqual( expect.arrayContaining([ expect.objectContaining({ id: shared.id, hasContent: true, canDelete: false }), ]), ); await rpc(app, cookie, "bots/remove", { botId: ownerBot.id, deleteMemories: true }, shared.id); expect( (await rpc(app, cookie, "spaces/list", {}, shared.id)).spaces.find( (space) => space.id === shared.id, )?.canDelete, ).toBe(true); const ownerRemoved = await rpc<{ ok: true; activeSpaceId: string }>( app, cookie, "spaces/remove", { spaceId: shared.id, }, ); expect(ownerRemoved.ok).toBe(true); await expect(handles.prisma.space.findUnique({ where: { id: shared.id } })).resolves.toBeNull(); }); it("blocks bot creation after empty space deletion is claimed", async () => { const cookie = await signup(app, `space-race-${stamp}@rakazo.test`, "Space Race"); const actor = await rpc(app, cookie, "me"); const space = await rpc(app, cookie, "spaces/create", { name: "Concurrent" }); await handles.prisma.computer.create({ data: { spaceId: space.id, userId: actor.userId, scopeKey: `space-race-scope-${stamp}`, homeKey: `space-race-home-${stamp}`, kind: "fake", providerRef: `space-race-provider-${stamp}`, }, }); const deleteInput = { currentSpaceId: space.id, userId: actor.userId, spaceId: space.id, }; const firstClaim = await claimEmptySpaceDeletionForMember(handles.prisma, deleteInput); expect(firstClaim.recovered).toBe(false); expect(firstClaim.computers).toEqual([ expect.objectContaining({ providerRef: `space-race-provider-${stamp}` }), ]); await expect( renewSpaceDeletionClaim(handles.prisma, { ...deleteInput, claimId: firstClaim.claimId }), ).resolves.toBe(true); const claimedNavigation = await rpc(app, cookie, "spaces/list", {}, space.id); expect(claimedNavigation.spaces.find((item) => item.id === space.id)?.canDelete).toBe(false); const blockedCreate = await raw(app, cookie, "bots/create", botInput("Racing bot"), space.id); expect(blockedCreate.ok).toBe(false); expect(blockedCreate.status).toBe(409); expect(await handles.prisma.bot.count({ where: { spaceId: space.id } })).toBe(0); expect( await handles.prisma.computer.findFirst({ where: { spaceId: space.id }, select: { providerRef: true }, }), ).toEqual({ providerRef: `space-race-provider-${stamp}` }); await handles.prisma.space.update({ where: { id: space.id }, data: { deletingAt: new Date(Date.now() - 6 * 60_000) }, }); const replacementClaim = await claimEmptySpaceDeletionForMember(handles.prisma, deleteInput); expect(replacementClaim.recovered).toBe(true); await expect( deleteEmptySpaceForMember(handles.prisma, { ...deleteInput, claimId: firstClaim.claimId }), ).rejects.toThrow("Space deletion is already in progress"); await releaseSpaceDeletionClaim(handles.prisma, { ...deleteInput, claimId: firstClaim.claimId, }); expect( await handles.prisma.space.findUnique({ where: { id: space.id }, select: { deletionClaimId: true }, }), ).toEqual({ deletionClaimId: replacementClaim.claimId }); await releaseSpaceDeletionClaim(handles.prisma, { ...deleteInput, claimId: replacementClaim.claimId, }); const created = await raw(app, cookie, "bots/create", botInput("After retry"), space.id); expect(created.ok).toBe(true); }); it("keeps a space claimed after ambiguous sandbox teardown failure", async () => { const cookie = await signup(app, `space-teardown-${stamp}@rakazo.test`, "Teardown"); const actor = await rpc(app, cookie, "me"); const space = await rpc(app, cookie, "spaces/create", { name: "Teardown" }); await handles.prisma.computer.create({ data: { spaceId: space.id, userId: actor.userId, scopeKey: `space-teardown-scope-${stamp}`, homeKey: `space-teardown-home-${stamp}`, kind: "fake", providerRef: `space-teardown-provider-${stamp}`, }, }); const destroy = vi .spyOn(handles.sandbox, "destroy") .mockRejectedValueOnce(new Error("ambiguous provider failure")); try { const removed = await raw(app, cookie, "spaces/remove", { spaceId: space.id }, space.id); expect(removed.status).toBe(500); const lifecycle = await handles.prisma.space.findUnique({ where: { id: space.id }, select: { deletingAt: true, deletionClaimId: true }, }); expect(lifecycle?.deletingAt).toBeInstanceOf(Date); expect(lifecycle?.deletionClaimId).toBeTruthy(); expect( await handles.prisma.computer.findFirst({ where: { spaceId: space.id }, select: { providerRef: true }, }), ).toEqual({ providerRef: `space-teardown-provider-${stamp}` }); const blockedCreate = await raw( app, cookie, "bots/create", botInput("Blocked after teardown"), space.id, ); expect(blockedCreate.ok).toBe(false); } finally { destroy.mockRestore(); const lifecycle = await handles.prisma.space.findUnique({ where: { id: space.id }, select: { deletionClaimId: true }, }); if (lifecycle?.deletionClaimId) { await releaseSpaceDeletionClaim(handles.prisma, { currentSpaceId: space.id, spaceId: space.id, userId: actor.userId, claimId: lifecycle.deletionClaimId, }); } } }); it("times out a hung sandbox teardown without unblocking the space", async () => { const cookie = await signup(app, `space-deadline-${stamp}@rakazo.test`, "Deadline"); const actor = await rpc(app, cookie, "me"); const space = await rpc(app, cookie, "spaces/create", { name: "Deadline" }); await handles.prisma.computer.create({ data: { spaceId: space.id, userId: actor.userId, scopeKey: `space-deadline-scope-${stamp}`, homeKey: `space-deadline-home-${stamp}`, kind: "fake", providerRef: `space-deadline-provider-${stamp}`, }, }); process.env.SPACE_TEARDOWN_TIMEOUT_MS = "50"; const destroy = vi .spyOn(handles.sandbox, "destroy") .mockImplementation(() => new Promise(() => undefined)); try { const removed = await raw(app, cookie, "spaces/remove", { spaceId: space.id }, space.id); expect(removed.status).toBe(500); const lifecycle = await handles.prisma.space.findUnique({ where: { id: space.id }, select: { deletingAt: true, deletionClaimId: true }, }); expect(lifecycle?.deletingAt).toBeInstanceOf(Date); expect(lifecycle?.deletionClaimId).toBeTruthy(); const blockedCreate = await raw( app, cookie, "bots/create", botInput("Blocked during teardown"), space.id, ); expect(blockedCreate.status).toBe(409); } finally { destroy.mockRestore(); delete process.env.SPACE_TEARDOWN_TIMEOUT_MS; const lifecycle = await handles.prisma.space.findUnique({ where: { id: space.id }, select: { deletionClaimId: true }, }); if (lifecycle?.deletionClaimId) { await releaseSpaceDeletionClaim(handles.prisma, { currentSpaceId: space.id, spaceId: space.id, userId: actor.userId, claimId: lifecycle.deletionClaimId, }); } } }); it("validates custom thinking against the saved connection capability", async () => { const cookie = await signup(app, `custom-thinking-${stamp}@rakazo.test`, "Custom Thinking"); const bot = await rpc(app, cookie, "bots/create", botInput("Thinking Bot")); const connection = { provider: "openai-compatible", modelId: "arbitrary-model", baseUrl: "http://localhost:8000/v1", }; await rpc(app, cookie, "models/connect", { ...connection, apiKey: "fake-saved-key", reasoning: false, }); await rpc(app, cookie, "models/connect", { ...connection, reasoning: true }); const actor = await rpc(app, cookie, "me"); expect( await handles.executor.resolveModel({ userId: actor.userId, spaceId: actor.spaceId, botId: bot.id, }), ).toMatchObject({ apiKey: "fake-saved-key", reasoning: true }); const update = { botId: bot.id, modelProvider: connection.provider, modelId: connection.modelId, }; expect( await rpc(app, cookie, "bots/update", { ...update, thinkingLevel: "low" }), ).toMatchObject({ thinkingLevel: "low" }); expect( (await raw(app, cookie, "bots/update", { ...update, thinkingLevel: "xhigh" })).status, ).toBe(400); await rpc(app, cookie, "models/connect", { ...connection, reasoning: false }); expect( (await raw(app, cookie, "bots/update", { ...update, thinkingLevel: "low" })).status, ).toBe(400); expect( await rpc(app, cookie, "bots/update", { ...update, thinkingLevel: "off" }), ).toMatchObject({ thinkingLevel: "off" }); }); it("validates per-bot model overrides against connected providers and catalog", async () => { const cookie = await signup(app, `bot-model-${stamp}@rakazo.test`, "Bot Model"); const bot = await rpc< Bot & { modelProvider: string | null; modelId: string | null; thinkingLevel: string | null; } >(app, cookie, "bots/create", botInput("Model Bot")); await rpc(app, cookie, "models/connect", { provider: "xai", apiKey: "fake-xai-key-not-real", label: "xAI", modelId: "grok-4.6", }); const updated = await rpc< Bot & { modelProvider: string | null; modelId: string | null; thinkingLevel: string | null; } >(app, cookie, "bots/update", { botId: bot.id, modelProvider: "xai", modelId: "grok-4.6", thinkingLevel: "high", }); expect(updated).toMatchObject({ modelProvider: "xai", modelId: "grok-4.6", thinkingLevel: "high", }); const unknown = await raw(app, cookie, "bots/update", { botId: bot.id, modelProvider: "xai", modelId: "not-a-real-grok", }); expect(unknown.status).toBeGreaterThanOrEqual(400); expect(await unknown.text()).toMatch(/unknown model/i); const disconnected = await raw(app, cookie, "bots/update", { botId: bot.id, modelProvider: "anthropic", modelId: "claude-opus-4-6", }); expect(disconnected.status).toBeGreaterThanOrEqual(400); expect(await disconnected.text()).toMatch(/connect/i); const partialClear = await raw(app, cookie, "bots/update", { botId: bot.id, modelId: null, }); expect(partialClear.status).toBeGreaterThanOrEqual(400); expect(await partialClear.text()).toMatch(/both be set or both cleared/i); }); it("chooses the newest duplicate provider credential when selecting a default", async () => { const cookie = await signup(app, `model-duplicates-${stamp}@rakazo.test`, "Model Duplicates"); const actor = await rpc(app, cookie, "me"); const olderSecret = await handles.prisma.secret.create({ data: { userId: actor.userId, spaceId: null, kind: "model", ciphertext: "encrypted-older-key", }, }); const newerSecret = await handles.prisma.secret.create({ data: { userId: actor.userId, spaceId: null, kind: "model", ciphertext: "encrypted-newer-key", }, }); const older = await handles.prisma.userModelCredential.create({ data: { userId: actor.userId, provider: "duplicate-provider", label: "Older", secretId: olderSecret.id, createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-02T00:00:00.000Z"), }, }); const newer = await handles.prisma.userModelCredential.create({ data: { userId: actor.userId, provider: "duplicate-provider", label: "Newer", secretId: newerSecret.id, createdAt: new Date("2026-02-01T00:00:00.000Z"), updatedAt: new Date("2026-02-02T00:00:00.000Z"), }, }); await handles.prisma.spaceModelPreference.create({ data: { spaceId: actor.spaceId, userId: actor.userId, credentialId: older.id, modelId: "older/model", isDefault: true, }, }); await rpc(app, cookie, "models/setDefault", { provider: "duplicate-provider", modelId: "newer/selected", }); const preferences = await handles.prisma.spaceModelPreference.findMany({ where: { userId: actor.userId, spaceId: actor.spaceId }, }); expect(preferences.filter((row) => row.isDefault).map((row) => row.credentialId)).toEqual([ newer.id, ]); expect(preferences.find((row) => row.credentialId === newer.id)).toMatchObject({ isDefault: true, modelId: "newer/selected", }); expect(preferences.find((row) => row.credentialId === older.id)).toMatchObject({ isDefault: false, modelId: "older/model", }); const listed = await rpc(app, cookie, "models/credentials"); expect( listed.filter((row) => row.provider === "duplicate-provider").map((row) => row.id), ).toEqual([newer.id, older.id]); }); it("restricts deployment settings to the deployment owner", async () => { const owner = await signup(app, `deployment-owner-${stamp}@rakazo.test`, "Deployment Owner"); const other = await signup(app, `deployment-other-${stamp}@rakazo.test`, "Deployment Other"); const ownerActor = await rpc(app, owner, "me"); const otherActor = await rpc(app, other, "me"); // This test changes a live allowlist; the operator has already proved // ownership of the mailbox. Endpoint verification has offline auth tests. await handles.prisma.user.update({ where: { id: ownerActor.userId }, data: { emailVerified: true }, }); await handles.prisma.deploymentSettings.update({ where: { id: "default" }, data: { ownerUserId: ownerActor.userId, signupsEnabled: true, signupAllowlist: "", }, }); expect(otherActor.userId).not.toBe(ownerActor.userId); await rpc(app, owner, "deployment/get"); await expectDenied(app, other, "deployment/get", {}); await expectDenied(app, other, "deployment/update", { signupsEnabled: false, signupAllowlist: ["attacker@example.test"], }); await expectForbidden(app, other, "updater/status", {}); await expectForbidden(app, other, "updater/check", {}); await expectForbidden(app, other, "updater/apply", {}); expect( await handles.prisma.deploymentSettings.findUniqueOrThrow({ where: { id: "default" } }), ).toMatchObject({ signupsEnabled: true, signupAllowlist: "" }); try { await rpc(app, owner, "deployment/update", { signupsEnabled: false }); const closedSignup = await app.request("/api/auth/sign-up/email", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: `closed-${stamp}@rakazo.test`, password: "password123", name: "Closed Signup", }), }); expect(closedSignup.status).toBe(400); expect(await closedSignup.text()).toContain("Registration is closed"); const approvedEmail = `approved-${stamp}@example.test`; await rpc(app, owner, "deployment/update", { signupsEnabled: true, signupAllowlist: [approvedEmail], }); const disallowedSignup = await app.request("/api/auth/sign-up/email", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: `not-approved-${stamp}@rakazo.test`, password: "password123", name: "Disallowed Signup", }), }); expect(disallowedSignup.status).toBe(400); expect(await disallowedSignup.text()).toContain("Email is not allowed to register"); const unverifiedSignup = await app.request("/api/auth/sign-up/email", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: approvedEmail, password: "password123", name: "Approved Signup", }), }); expect(unverifiedSignup.status).toBe(400); expect(await unverifiedSignup.text()).toContain("Registration requires email delivery"); } finally { await rpc(app, owner, "deployment/update", { signupsEnabled: true, signupAllowlist: [], }); } }); it("lets only the deployment owner link a host folder and read its files", async () => { const owner = await signup(app, `folder-owner-${stamp}@rakazo.test`, "Folder Owner"); const member = await signup(app, `folder-member-${stamp}@rakazo.test`, "Folder Member"); const ownerActor = await rpc(app, owner, "me"); const settings = await handles.prisma.deploymentSettings.findUniqueOrThrow({ where: { id: "default" }, select: { ownerUserId: true }, }); await handles.prisma.deploymentSettings.update({ where: { id: "default" }, data: { ownerUserId: ownerActor.userId }, }); // Stands in for any folder the API process can read, such as its own checkout. const hostFolder = realpathSync(mkdtempSync(path.join(tmpdir(), "rakazo-authz-host-"))); writeFileSync(path.join(hostFolder, ".env"), "HOST_SECRET=not-a-real-secret\n"); try { await expectForbidden(app, member, "spaces/create", { name: "Member folder", workspacePath: hostFolder, }); const memberNavigation = await rpc(app, member, "spaces/list"); expect(memberNavigation.spaces.map((space) => space.name)).not.toContain("Member folder"); // A space linked before only the owner could link stays closed to the member. const legacy = await rpc(app, member, "spaces/create", { name: "Legacy folder" }); await handles.prisma.space.update({ where: { id: legacy.id }, data: { workspacePath: hostFolder }, }); const memberBot = await rpc( app, member, "bots/create", botInput("Member Folder Reader"), legacy.id, ); await expectForbidden( app, member, "computer/files", { botId: memberBot.id, path: "project" }, legacy.id, ); await expectForbidden( app, member, "computer/readFile", { botId: memberBot.id, path: "project/.env" }, legacy.id, ); const legacyNavigation = await rpc(app, member, "spaces/list"); expect(legacyNavigation.spaces.find((space) => space.id === legacy.id)?.workspace).toBeNull(); // A bad folder is rejected before the project exists, so fixing it and retrying leaves one. for (const badPath of [path.join(hostFolder, "missing"), path.join(hostFolder, ".env")]) { const rejected = await raw(app, owner, "spaces/create", { name: "Owner folder", workspacePath: badPath, }); expect(rejected.status, badPath).toBe(400); } const linked = await rpc(app, owner, "spaces/create", { name: "Owner folder", workspacePath: hostFolder, }); expect(linked.workspace?.path).toBe(hostFolder); const ownerNavigation = await rpc(app, owner, "spaces/list"); expect( ownerNavigation.spaces .filter((space) => space.name === "Owner folder") .map((space) => space.workspace?.path), ).toEqual([hostFolder]); const ownerBot = await rpc( app, owner, "bots/create", botInput("Owner Folder Reader"), linked.id, ); const listing = await rpc>( app, owner, "computer/files", { botId: ownerBot.id, path: "project" }, linked.id, ); expect(listing.map((entry) => entry.path)).toContain("project/.env"); const file = await rpc<{ content: string }>( app, owner, "computer/readFile", { botId: ownerBot.id, path: "project/.env" }, linked.id, ); expect(file.content).toContain("HOST_SECRET"); } finally { await handles.prisma.deploymentSettings.update({ where: { id: "default" }, data: { ownerUserId: settings.ownerUserId }, }); rmSync(hostFolder, { recursive: true, force: true }); } }); }); function botInput(name: string) { return { name, title: "", description: "", instructions: "", notifyOnFinish: false, }; } function routineInput(botId: string) { return { botId, name: "Owner Routine", prompt: "owner-only prompt", crons: ["0 9 * * 1"], timezone: "UTC", notify: false, active: false, }; } function skillPlaybookInput() { return { whenToUse: "When needed", inputs: ["example"], steps: ["Do the thing"], howToCheck: "Verify result", whatToReturn: "Summary", approvalBoundaries: "Ask first", failureHandling: "Stop and ask", }; } function capabilityInput(name: string) { return { kind: "skill", name, source: "test://authorization", config: {} }; } function connectionInput(displayName: string) { return { provider: "test-provider", displayName }; } /** * Move a user onto another organization's space. * Inserting `member` already grants the organization's default space * (`ensure_default_space_membership`). Creating that row again hits * `space_members_spaceId_userId_key` on a shared Postgres. */ async function joinMemberOnSpace( prisma: AppHandles["prisma"], input: { id: string; organizationId: string; spaceId: string; userId: string }, ) { await prisma.spaceMember.deleteMany({ where: { userId: input.userId } }); await prisma.member.deleteMany({ where: { userId: input.userId } }); await prisma.member.create({ data: { id: input.id, organizationId: input.organizationId, userId: input.userId, role: "member", createdAt: new Date(), }, }); const where = { spaceId_userId: { spaceId: input.spaceId, userId: input.userId } }; const existing = await prisma.spaceMember.findUnique({ where }); if (existing) return existing; return prisma.spaceMember.create({ data: { id: `${input.id}-space`, spaceId: input.spaceId, organizationId: input.organizationId, userId: input.userId, role: "member", createdAt: new Date(), }, }); } async function signup(app: App, email: string, name: string) { const response = await app.request("/api/auth/sign-up/email", { method: "POST", headers: { "content-type": "application/json", origin: "http://127.0.0.1:5173" }, body: JSON.stringify({ email, password: "password12", name }), }); if (response.status >= 400) { throw new Error(`signup failed ${response.status}: ${await response.text()}`); } return sessionCookieHeader(response); } async function raw( app: App, cookie: string, procedure: string, body: unknown = {}, spaceId?: string, ) { return app.request(`/rpc/${procedure}`, { method: "POST", headers: { "content-type": "application/json", ...(cookie ? { cookie } : {}), ...(spaceId ? { "x-rakazo-space-id": spaceId } : {}), origin: "http://127.0.0.1:5173", }, body: JSON.stringify({ json: body ?? {} }), }); } async function rpc( app: App, cookie: string, procedure: string, body: unknown = {}, spaceId?: string, ): Promise { const response = await raw(app, cookie, procedure, body, spaceId); const text = await response.text(); const payload = JSON.parse(text) as { json?: T; error?: { message?: string } }; if (response.status >= 400 || payload.error) { throw new Error(`${procedure} ${response.status}: ${payload.error?.message ?? text}`); } return payload.json as T; } async function expectDenied( app: App, cookie: string, procedure: string, body: unknown, spaceId?: string, ) { const response = await raw(app, cookie, procedure, body, spaceId); if (procedure === "threads/subscribe" && response.status === 200) { // Streaming transports commit the HTTP 200 before advancing the async iterator. The // ownership error is therefore encoded in the iterator response instead of the status. expect(await response.text(), procedure).toMatch(/error|forbidden|not.found|unauthorized/i); return; } expect(response.status, procedure).toBeGreaterThanOrEqual(400); } async function expectForbidden( app: App, cookie: string, procedure: string, body: unknown, spaceId?: string, ) { const response = await raw(app, cookie, procedure, body, spaceId); expect(response.status, procedure).toBe(403); expect(await response.text(), procedure).toMatch(/forbidden/i); } interface Actor { userId: string; spaceId: string; } interface ModelCredential { id: string; provider: string; label: string; hasKey: boolean; isDefault: boolean; } interface Bot { id: string; } function exhaustiveProtectedCalls< const Calls extends ReadonlyArray, >(calls: Calls & (ProtectedRpcPath extends Calls[number][0] ? unknown : never)): Calls { return calls; }