Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
68e962f
feat: add isolated private spaces
elie222 Aug 30, 2026
c8aeb49
fix: scope voice HTTP and private-space review follow-ups
cursoragent Aug 30, 2026
f8c03cf
fix(web): keep URL constructor in voice workspace header test
cursoragent Aug 30, 2026
70860d4
fix: address CodeRabbit private-space review notes
cursoragent Aug 30, 2026
ee442a7
fix(api): serialize private-space create against the membership cap
cursoragent Aug 30, 2026
363cf4d
fix(mobile): clear credentials before switching API endpoints
cursoragent Aug 30, 2026
e20800e
fix(web): type voice header test fetch mocks for tsc
cursoragent Aug 30, 2026
ad4dfb8
fix: preserve private-space request boundaries
elie222 Aug 30, 2026
5e45c92
fix(mobile): refuse endpoint switches when credentials cannot clear
cursoragent Aug 30, 2026
92a44cd
fix(mobile): restore credentials when endpoint clear is partial
cursoragent Aug 30, 2026
5d44102
fix(mobile): preserve active session on cleanup failure
elie222 Aug 30, 2026
f87722d
fix: harden endpoint switch rollback and voice workspace binding
cursoragent Aug 30, 2026
eeb086f
fix: clear default-space selection and peek session fallbacks
cursoragent Aug 30, 2026
02f1c77
fix(mobile): include session fallback when snapshotting credentials
cursoragent Aug 30, 2026
9a7c927
test(mobile): cover consecutive failed endpoint switches
cursoragent Aug 30, 2026
9900ddd
style(mobile): format consecutive endpoint-switch test
cursoragent Aug 30, 2026
bcf619d
fix: bind voice runs and verify endpoint rollback
elie222 Aug 30, 2026
c32e315
fix(web): full-reload when returning to primary workspace
cursoragent Aug 30, 2026
9f997a9
fix(web): reload only when private-space boundary changes
cursoragent Aug 30, 2026
7a6e056
style(web): organize private-space imports in Shell
cursoragent Aug 30, 2026
d2bc126
fix(web): preserve active private-space navigation
elie222 Aug 30, 2026
d4cf0bd
fix(web): persist workspace id and reload only on boundary change
cursoragent Aug 30, 2026
6f1dc9c
fix(mobile): peek SecureStore space when snapshotting endpoint rollback
cursoragent Aug 30, 2026
9cb684a
fix(mobile): preserve persisted workspace on rollback
elie222 Aug 30, 2026
9fec93d
fix(web): ignore private-space localStorage write failures
cursoragent Aug 30, 2026
8777324
fix(web): fail closed on workspace storage errors
elie222 Aug 30, 2026
21e5dd5
fix(web): require workspace persistence before navigation
elie222 Aug 30, 2026
c34e709
fix(web): block all space navigation when selection cannot be stored
cursoragent Aug 30, 2026
4053937
Merge origin/main into private-bot-groups
elie222 Aug 30, 2026
1868cca
test: assert private-space notification setting
elie222 Aug 30, 2026
4b77e24
fix(mobile): preserve session on failed endpoint switch
elie222 Aug 30, 2026
9068511
Merge remote-tracking branch 'origin/main' into private-bot-groups
elie222 Aug 30, 2026
4a0a11e
Merge remote-tracking branch 'origin/main' into private-bot-groups
elie222 Aug 30, 2026
3a66f00
test(web): intercept consolidated roster refresh
elie222 Aug 30, 2026
5510e8d
Merge origin/main into private-bot-groups
elie222 Aug 30, 2026
79fa6db
fix(mobile): keep invalidated sessions fail closed
elie222 Aug 30, 2026
9607099
test(mobile): reset session state between cases
elie222 Aug 30, 2026
e8b03d9
feat(db): model private spaces within organizations
elie222 Aug 30, 2026
ce1fa02
test(web): capture the single-space sidebar
elie222 Aug 30, 2026
e248f97
feat(spaces): create spaces from chat
elie222 Aug 30, 2026
6c5d9df
fix(mobile): pin space during speech playback
elie222 Aug 30, 2026
9cccfd4
fix(mobile): bind speech to its API endpoint
elie222 Aug 30, 2026
70798c8
refactor(db): share thread listing projections
elie222 Aug 30, 2026
4ca6ae8
docs(db): correct workspace migration lock notes
elie222 Aug 30, 2026
5aff6f3
refactor(db): drop unused private-space listing surface
elie222 Aug 30, 2026
ba86b50
refactor(spaces): align schema and terminology
elie222 Aug 30, 2026
330da29
test(adapters): allow safe platform-specific containment
elie222 Aug 30, 2026
82cd411
fix(mobile): preserve space across endpoint rollback
elie222 Aug 30, 2026
ff983e5
fix(spaces): close persistence and migration review gaps
elie222 Aug 30, 2026
a5aebb2
fix(mobile): discard malformed space recovery
elie222 Aug 30, 2026
dc4699c
fix(mobile): clear recovery before space switch
elie222 Aug 30, 2026
2b2ae2e
fix(mobile): resume notifications after endpoint rollback
elie222 Aug 30, 2026
daa2c67
fix(mobile): scope native notifications to spaces
elie222 Aug 30, 2026
575a148
chore: ignore Python bytecode caches
elie222 Aug 30, 2026
d225065
Make spaces the application privacy boundary
elie222 Aug 30, 2026
48e5590
Merge origin/main into private-bot-groups
elie222 Aug 30, 2026
6f7a132
fix(db): harden space privacy migration
elie222 Aug 30, 2026
f4efdee
fix(mobile): preserve notification space context
elie222 Aug 30, 2026
4e540d6
fix: preserve space upgrade compatibility
elie222 Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.deepsec/
node_modules
__pycache__/
*.py[cod]
.turbo
dist
build
Expand Down
20 changes: 10 additions & 10 deletions apps/api/src/agent-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
const row = await prisma.agentSkill.findFirst({
where: {
id: skillId,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
},
});
Expand All @@ -127,7 +127,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
return {
async list(actor: Actor): Promise<Omit<AgentSkill, "content">[]> {
const rows = await prisma.agentSkill.findMany({
where: { workspaceId: actor.workspaceId, userId: actor.userId },
where: { spaceId: actor.spaceId, userId: actor.userId },
orderBy: [{ name: "asc" }, { id: "asc" }],
});
const catalog = [...builtinCatalog(), ...rows.map(mapAgentSkill)].map(
Expand All @@ -138,7 +138,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {

async listWithContent(actor: Actor): Promise<AgentSkill[]> {
const rows = await prisma.agentSkill.findMany({
where: { workspaceId: actor.workspaceId, userId: actor.userId },
where: { spaceId: actor.spaceId, userId: actor.userId },
orderBy: [{ name: "asc" }, { id: "asc" }],
});
return [...builtinCatalog(), ...rows.map(mapAgentSkill)];
Expand All @@ -160,7 +160,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
if (builtin) return builtin;
const row = await prisma.agentSkill.findFirst({
where: {
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
name: { equals: name, mode: "insensitive" },
},
Expand All @@ -176,7 +176,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
const resolved = resolveSkillContent(input);
const clash = await prisma.agentSkill.findFirst({
where: {
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
name: { equals: resolved.name, mode: "insensitive" },
},
Expand All @@ -190,7 +190,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
try {
const row = await prisma.agentSkill.create({
data: {
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
name: resolved.name,
description: resolved.description,
Expand Down Expand Up @@ -229,7 +229,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
if (resolved.name.toLowerCase() !== existing.name.toLowerCase()) {
const clash = await prisma.agentSkill.findFirst({
where: {
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
name: { equals: resolved.name, mode: "insensitive" },
NOT: { id: existing.id },
Expand All @@ -247,7 +247,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
const updated = await prisma.agentSkill.updateMany({
where: {
id: existing.id,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
source: "user",
},
Expand All @@ -272,7 +272,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
const row = await prisma.agentSkill.findFirst({
where: {
id: existing.id,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
},
});
Expand All @@ -288,7 +288,7 @@ export function createAgentSkillsService(prisma: PrismaClient) {
const deleted = await prisma.agentSkill.deleteMany({
where: {
id: existing.id,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
source: "user",
},
Expand Down
17 changes: 10 additions & 7 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ import {
type RemoteConnectorDependencies,
ScriptedAgentRuntime,
SendBlueMessagingProvider,
SpaceMemoryProviderResolver,
sendBlueConfigFromEnv,
WorkspaceMemoryProviderResolver,
} from "@rakazo/adapters";
import { blockedAuthPaths, createAuth } from "@rakazo/auth";
import { signupPolicyFromEnv } from "@rakazo/core";
Expand Down Expand Up @@ -159,7 +159,7 @@ export async function createApp(
prisma,
});
const mcpOAuth = new McpOAuthBroker(prisma, secrets, remoteConnectors);
const memoryProviders = new WorkspaceMemoryProviderResolver(prisma, secrets);
const memoryProviders = new SpaceMemoryProviderResolver(prisma, secrets);
const oauthLogins = new PiOAuthLogins();
const home = new LocalAgentHomeStore(env.dataDir);
const artifacts = new LocalArtifactStore(env.dataDir);
Expand Down Expand Up @@ -215,7 +215,7 @@ export async function createApp(
beforeDeleteUser: async (userId) => {
const bots = await prisma.bot.findMany({
where: { userId },
select: { id: true, workspaceId: true, name: true, archivedAt: true },
select: { id: true, spaceId: true, name: true, archivedAt: true },
});
await Promise.all(
bots.map((bot) =>
Expand All @@ -225,7 +225,7 @@ export async function createApp(
{
operationId: `account-delete:${userId}`,
traceId: `account-delete:${userId}`,
workspaceId: bot.workspaceId,
spaceId: bot.spaceId,
userId,
botId: bot.id,
signal: new AbortController().signal,
Expand Down Expand Up @@ -333,8 +333,9 @@ export async function createApp(
});
app.use("/rpc/*", async (c, next) => {
const session = await auth.api.getSession({ headers: sessionHeaders(c.req.raw) });
const requestedSpaceId = c.req.header("x-rakazo-space-id");
const actor = session?.user
? await requireMembership(prisma, session.user.id).catch(() => null)
? await requireMembership(prisma, session.user.id, requestedSpaceId).catch(() => null)
: null;
const { matched, response } = await rpc.handle(c.req.raw, {
prefix: "/rpc",
Expand All @@ -346,7 +347,9 @@ export async function createApp(
mountVoiceHttpRoutes(app, { prisma, secrets }, async (c) => {
const session = await auth.api.getSession({ headers: sessionHeaders(c.req.raw) });
if (!session?.user) return null;
return requireMembership(prisma, session.user.id).catch(() => null);
return requireMembership(prisma, session.user.id, c.req.header("x-rakazo-space-id")).catch(
() => null,
);
});
mountWebhookHttpRoutes(app, { prisma, secrets, events, jobs });
// The phone webhook only exists when the messaging surface is enabled.
Expand Down Expand Up @@ -376,7 +379,7 @@ export async function createApp(
{
operationId,
traceId: operationId,
workspaceId: "",
spaceId: "",
userId: "",
// Cosmetic side call: bound it so a stalled vendor response
// can never pin the webhook handler's event loop slot.
Expand Down
14 changes: 7 additions & 7 deletions apps/api/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function adapterContext(actor: Actor, botId: string, operationId: string) {
return {
operationId,
traceId: operationId,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
botId,
signal: new AbortController().signal,
Expand Down Expand Up @@ -47,7 +47,7 @@ export async function createOwnedArtifact(
const row = await deps.prisma.artifact
.create({
data: {
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
botId: input.botId,
groupId: input.groupId,
Expand Down Expand Up @@ -87,15 +87,15 @@ export async function getOwnedArtifact(
id: input.artifactId,
botId: input.botId,
groupId: null,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
},
});
if (!row) throw new IsolationError();
return readArtifact(deps.artifacts, actor, row, input.botId);
}

export async function getWorkspaceArtifact(
export async function getSpaceArtifact(
deps: {
prisma: PrismaClient;
artifacts: ArtifactStore;
Expand All @@ -107,7 +107,7 @@ export async function getWorkspaceArtifact(
where: {
id: input.artifactId,
groupId: input.groupId,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
},
});
Expand Down Expand Up @@ -193,7 +193,7 @@ export async function resolveSendAttachments(
id: { in: ids },
botId,
groupId: null,
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
},
});
Expand All @@ -213,7 +213,7 @@ export async function resolveGroupSendAttachments(
const rows = await deps.prisma.artifact.findMany({
where: {
id: { in: ids },
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
OR: [
{ groupId },
Expand Down
18 changes: 9 additions & 9 deletions apps/api/src/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const APP_NAMES: Record<string, string> = {

async function requireBotThread(deps: OnboardingDeps, actor: Actor, botId: string) {
const bot = await deps.prisma.bot.findFirst({
where: { id: botId, workspaceId: actor.workspaceId, userId: actor.userId },
where: { id: botId, spaceId: actor.spaceId, userId: actor.userId },
include: { thread: true },
});
if (!bot?.thread) throw new IsolationError();
Expand All @@ -88,7 +88,7 @@ async function requireBotThread(deps: OnboardingDeps, actor: Actor, botId: strin

async function post(
deps: OnboardingDeps,
target: { workspaceId: string; botId: string; threadId: string },
target: { spaceId: string; botId: string; threadId: string },
blocks: MessageBlock[],
): Promise<string> {
const message = await createThreadMessage(deps.prisma, {
Expand All @@ -97,7 +97,7 @@ async function post(
blocks,
});
await deps.events.append({
workspaceId: target.workspaceId,
spaceId: target.spaceId,
threadId: target.threadId,
botId: target.botId,
type: "thread.message.created",
Expand All @@ -108,13 +108,13 @@ async function post(

async function updateBlocks(
deps: OnboardingDeps,
target: { workspaceId: string; botId: string; threadId: string },
target: { spaceId: string; botId: string; threadId: string },
messageId: string,
blocks: MessageBlock[],
): Promise<void> {
await deps.prisma.message.update({ where: { id: messageId }, data: { blocks } });
await deps.events.append({
workspaceId: target.workspaceId,
spaceId: target.spaceId,
threadId: target.threadId,
botId: target.botId,
type: "thread.message.updated",
Expand All @@ -135,7 +135,7 @@ export async function startOnboarding(
select: { name: true },
});
const firstName = (user?.name ?? "there").split(/\s+/)[0];
const target = { workspaceId: actor.workspaceId, botId: bot.id, threadId: thread.id };
const target = { spaceId: actor.spaceId, botId: bot.id, threadId: thread.id };
await post(deps, target, [
{ kind: "text", text: `Hey ${firstName}. Fresh start on my side, so I’ll keep this short.` },
]);
Expand All @@ -157,7 +157,7 @@ export async function chooseFocus(
const option = FOCUS_OPTIONS.find((entry) => entry.id === optionId);
if (!option) throw new IsolationError();
const { bot, thread } = await requireBotThread(deps, actor, botId);
const target = { workspaceId: actor.workspaceId, botId: bot.id, threadId: thread.id };
const target = { spaceId: actor.spaceId, botId: bot.id, threadId: thread.id };

const recent = await deps.prisma.message.findMany({
where: { threadId: thread.id },
Expand Down Expand Up @@ -186,7 +186,7 @@ export async function chooseFocus(
.catalog({
operationId: "onboarding.choose",
traceId: "onboarding.choose",
workspaceId: actor.workspaceId,
spaceId: actor.spaceId,
userId: actor.userId,
botId: bot.id,
signal: new AbortController().signal,
Expand Down Expand Up @@ -231,7 +231,7 @@ export async function markAppConnected(
provider: string,
): Promise<void> {
const { bot, thread } = await requireBotThread(deps, actor, botId);
const target = { workspaceId: actor.workspaceId, botId: bot.id, threadId: thread.id };
const target = { spaceId: actor.spaceId, botId: bot.id, threadId: thread.id };
const messages = await deps.prisma.message.findMany({
where: { threadId: thread.id },
select: { id: true, blocks: true },
Expand Down
20 changes: 10 additions & 10 deletions apps/api/src/persist-memory-provider-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { persistMemoryProviderConfig, updateMemoryProviderDefaultScope } from ".

const actor = {
userId: "user-1",
workspaceId: "ws-1",
spaceId: "ws-1",
email: "a@b.com",
isDeploymentOwner: false,
};
Expand All @@ -23,7 +23,7 @@ function makeDeps(
defaultMemoryScope: string;
updatedAt: Date;
};
workspaceOwner?: boolean;
spaceOwner?: boolean;
memberRole?: string;
} = {},
) {
Expand All @@ -47,14 +47,14 @@ function makeDeps(
},
);
const prisma = {
member: {
findFirst: vi
spaceMember: {
findUnique: vi
.fn()
.mockResolvedValue(
overrides.workspaceOwner === false ? null : { role: overrides.memberRole ?? "owner" },
overrides.spaceOwner === false ? null : { role: overrides.memberRole ?? "owner" },
),
},
workspaceMemoryConfig: { findUnique, update, upsert },
spaceMemoryConfig: { findUnique, update, upsert },
secret: { create: secretCreate, deleteMany: secretDeleteMany },
$transaction: vi.fn(),
};
Expand Down Expand Up @@ -86,10 +86,10 @@ function connectionInput(mode: "cloud" | "local", baseUrl?: string) {
}

describe("persistMemoryProviderConfig", () => {
it("rejects non-owners before probing or writing workspace configuration", async () => {
it("rejects non-owners before probing or writing Space configuration", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { deps, upsert } = makeDeps({ workspaceOwner: false });
const { deps, upsert } = makeDeps({ spaceOwner: false });

await expect(
persistMemoryProviderConfig(deps as never, actor, connectionInput("cloud")),
Expand Down Expand Up @@ -163,7 +163,7 @@ describe("persistMemoryProviderConfig", () => {
);
expect(upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { workspaceId: "ws-1" },
where: { spaceId: "ws-1" },
create: expect.objectContaining({
provider: "supermemory",
settings: { mode: "cloud", baseUrl: "https://api.supermemory.ai" },
Expand Down Expand Up @@ -225,7 +225,7 @@ describe("updateMemoryProviderDefaultScope", () => {
it("rejects non-owners without updating provider configuration", async () => {
const { deps, update } = makeDeps({
existing: { id: "cfg-1", secretId: "secret-existing" },
workspaceOwner: false,
spaceOwner: false,
});

await expect(
Expand Down
Loading
Loading