fix(api): redact memory provider details in aiConsent/status for members

This commit is contained in:
Ingo Wagner 2026-09-22 07:05:26 +00:00
parent c8c6d9e042
commit 928d9ad950
2 changed files with 46 additions and 110 deletions

View File

@ -25,13 +25,17 @@ export interface MemoryProviderConfigDeps {
) => ReturnType<typeof prepareMemoryProviderConnection>; ) => ReturnType<typeof prepareMemoryProviderConnection>;
} }
async function requireSpaceOwner(prisma: PrismaClient, actor: Actor): Promise<void> { export async function isSpaceOwner(prisma: PrismaClient, actor: Actor): Promise<boolean> {
const member = await prisma.spaceMember.findUnique({ const member = await prisma.spaceMember.findUnique({
where: { spaceId_userId: { spaceId: actor.spaceId, userId: actor.userId } }, where: { spaceId_userId: { spaceId: actor.spaceId, userId: actor.userId } },
select: { role: true }, select: { role: true },
}); });
const roles = member?.role.split(",").map((role) => role.trim()); const roles = member?.role.split(",").map((role) => role.trim());
if (!roles?.includes("owner")) throw new ORPCError("FORBIDDEN"); return roles?.includes("owner") ?? false;
}
async function requireSpaceOwner(prisma: PrismaClient, actor: Actor): Promise<void> {
if (!(await isSpaceOwner(prisma, actor))) throw new ORPCError("FORBIDDEN");
} }
export async function persistMemoryProviderConfig( export async function persistMemoryProviderConfig(
@ -134,15 +138,6 @@ export async function persistMemoryProviderConfig(
return serializeSpaceMemoryConfig(config); return serializeSpaceMemoryConfig(config);
} }
export async function getMemoryProviderConfig(
deps: Pick<MemoryProviderConfigDeps, "prisma">,
actor: Actor,
) {
await requireSpaceOwner(deps.prisma, actor);
const config = await findSpaceMemoryConfig(deps.prisma, actor.spaceId);
return config ? serializeSpaceMemoryConfig(config) : null;
}
export async function updateMemoryProviderDefaultScope( export async function updateMemoryProviderDefaultScope(
deps: MemoryProviderConfigDeps, deps: MemoryProviderConfigDeps,
actor: Actor, actor: Actor,

View File

@ -1,11 +1,10 @@
import { mkdtempSync, rmSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
import { ComposioEmulator, LocalAgentHomeStore, teamBotWorkspaceDirectory } from "@rakazo/adapters"; import { ComposioEmulator } from "@rakazo/adapters";
import type { appContract, Space, SpaceNavigation } from "@rakazo/contracts"; import type { AiConsentStatus, appContract, Space, SpaceNavigation } from "@rakazo/contracts";
import { import {
claimEmptySpaceDeletionForMember, claimEmptySpaceDeletionForMember,
computerHomeKey,
deleteEmptySpaceForMember, deleteEmptySpaceForMember,
releaseSpaceDeletionClaim, releaseSpaceDeletionClaim,
renewSpaceDeletionClaim, renewSpaceDeletionClaim,
@ -469,77 +468,6 @@ describeWithDatabase("API authorization and resource isolation", () => {
expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.toBeNull(); expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.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<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 () => { 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 owner = await signup(app, `approval-owner-${stamp}@rakazo.test`, "Approval Owner");
const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member"); const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member");
@ -581,21 +509,39 @@ describeWithDatabase("API authorization and resource isolation", () => {
).not.toBeNull(); ).not.toBeNull();
}); });
it("keeps routines private to each user in a shared Space", async () => { it("redacts memory provider details in aiConsent/status for non-owner members", async () => {
const owner = await signup(app, `routine-owner-${stamp}@rakazo.test`, "Routine Owner"); const owner = await signup(app, `ai-consent-owner-${stamp}@rakazo.test`, "Consent Owner");
const member = await signup(app, `routine-member-${stamp}@rakazo.test`, "Routine 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: "Consent shared" });
const shared = await rpc<Space>(app, owner, "spaces/create", { name: "Shared routines" });
const sharedRow = await handles.prisma.space.findUniqueOrThrow({ const sharedRow = await handles.prisma.space.findUniqueOrThrow({
where: { id: shared.id }, where: { id: shared.id },
select: { organizationId: true }, 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<Actor>(app, member, "me");
await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } });
await handles.prisma.member.create({ await handles.prisma.member.create({
data: { data: {
id: `routine-member-org-${stamp}`, id: `ai-consent-org-member-${stamp}`,
organizationId: sharedRow.organizationId, organizationId: sharedRow.organizationId,
userId: memberActor.userId, userId: memberActor.userId,
role: "member", role: "member",
@ -604,7 +550,7 @@ describeWithDatabase("API authorization and resource isolation", () => {
}); });
await handles.prisma.spaceMember.create({ await handles.prisma.spaceMember.create({
data: { data: {
id: `routine-member-space-${stamp}`, id: `ai-consent-space-member-${stamp}`,
spaceId: shared.id, spaceId: shared.id,
organizationId: sharedRow.organizationId, organizationId: sharedRow.organizationId,
userId: memberActor.userId, userId: memberActor.userId,
@ -613,31 +559,26 @@ describeWithDatabase("API authorization and resource isolation", () => {
}, },
}); });
const ownerBot = await rpc<Bot>( const ownerStatus = await rpc<AiConsentStatus>(
app, app,
owner, owner,
"bots/create", "aiConsent/status",
botInput("Owner shared bot"), { uses: ["memory"] },
shared.id, shared.id,
); );
const ownerRoutine = await rpc<{ id: string }>( const memberStatus = await rpc<AiConsentStatus>(
app,
owner,
"routines/create",
routineInput(ownerBot.id),
shared.id,
);
await expectDenied(
app, app,
member, member,
"routines/remove", "aiConsent/status",
{ routineId: ownerRoutine.id }, { uses: ["memory"] },
shared.id, shared.id,
); );
expect( const ownerMemory = ownerStatus.recipients.find((recipient) => recipient.use === "memory");
await handles.prisma.routine.findUnique({ where: { id: ownerRoutine.id } }), const memberMemory = memberStatus.recipients.find((recipient) => recipient.use === "memory");
).not.toBeNull(); 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 () => { it("keeps space data and computers behind the selected space boundary", async () => {