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
This commit is contained in:
Ingo Wagner 2026-09-22 07:15:22 +00:00
parent c8ea27c239
commit ec597a5c68
2 changed files with 79 additions and 2 deletions

View File

@ -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),

View File

@ -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<Actor>(app, owner, "me");
const memberActor = await rpc<Actor>(app, member, "me");
const shared = await rpc<Space>(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<Bot>(app, owner, "bots/create", botInput("Owner Bot"), shared.id);
const memberBot = await rpc<Bot>(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");