From ec597a5c688af39fe0c07fa37a46ebb678c002ac Mon Sep 17 00:00:00 2001 From: Ingo Wagner Date: Tue, 22 Sep 2026 07:15:22 +0000 Subject: [PATCH] fix(api): scope Team Computer export to exporting bot workspace - Add teamBotWorkspaceDirectory import to router.ts - Add exportOptions to scope team exports to one bot workspace - Add authorization test: member export only sees their workspace files - Add LocalAgentHomeStore and computerHomeKey imports to testkit --- apps/api/src/router.ts | 7 +- packages/testkit/src/authorization.test.ts | 74 +++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index a712c80..29a9a55 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -74,6 +74,7 @@ import { screenLeaseIdForRun, scriptedCatalogEntry, serializeModelSecret, + teamBotWorkspaceDirectory, takeoverLeaseMs, toComputerRef, touchRunningComputer, @@ -4477,6 +4478,10 @@ export function createRouter(deps: RouterDeps) { userId: context.actor.userId, signal: new AbortController().signal, }; + const exportOptions = + bot.computer.scope === "team" + ? { prefix: teamBotWorkspaceDirectory(input.botId) } + : undefined; const [memory, routines, files, history] = await Promise.all([ deps.prisma.memoryDocument.findMany({ where: { botId: input.botId, spaceId: context.actor.spaceId }, @@ -4486,7 +4491,7 @@ export function createRouter(deps: RouterDeps) { }), (async () => { const exported: Array<{ path: string; content: string }> = []; - for await (const file of deps.home.exportHome(homeKey, exportContext)) { + for await (const file of deps.home.exportHome(homeKey, exportContext, exportOptions)) { exported.push({ path: file.path, content: new TextDecoder().decode(file.content), diff --git a/packages/testkit/src/authorization.test.ts b/packages/testkit/src/authorization.test.ts index ced0fec..9e83e7e 100644 --- a/packages/testkit/src/authorization.test.ts +++ b/packages/testkit/src/authorization.test.ts @@ -1,10 +1,11 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { ComposioEmulator } from "@rakazo/adapters"; +import { ComposioEmulator, LocalAgentHomeStore, teamBotWorkspaceDirectory } from "@rakazo/adapters"; import type { AiConsentStatus, appContract, Space, SpaceNavigation } from "@rakazo/contracts"; import { claimEmptySpaceDeletionForMember, + computerHomeKey, deleteEmptySpaceForMember, releaseSpaceDeletionClaim, renewSpaceDeletionClaim, @@ -544,6 +545,77 @@ describeWithDatabase("API authorization and resource isolation", () => { expect(computer.controlBotId).toBe(ownerBot.id); }); + 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");