Skip to content

Commit 9e4cd91

Browse files
committed
refactor(memory): distinguish scoped note access controls
Rename the scoped-note control kind from memory_access to memory_notes_access so it is distinct from graph memory_scope. The HTTP path, delegation audience sim:memory-access, and semantic operation id are unchanged.
1 parent 291fdbb commit 9e4cd91

8 files changed

Lines changed: 104 additions & 91 deletions

File tree

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
import { resolveMemoryAccessContract } from '@/lib/api/contracts/mothership-memories'
1+
import { resolveMemoryNotesAccessContract } from '@/lib/api/contracts/mothership-memories'
22
import {
33
defineInternalJsonRoute,
44
internalOrchestrationErrorPolicy,
55
internalRateLimits,
66
} from '@/lib/api/server/routes'
77
import { internalCopilotAuth } from '@/lib/mothership/auth/internal'
88
import {
9-
MEMORY_ACCESS_AUDIENCE,
10-
resolveMemoryAccess,
11-
resolveMemoryAccessOperation,
9+
MEMORY_NOTES_ACCESS_AUDIENCE,
10+
resolveMemoryNotesAccess,
11+
resolveMemoryNotesAccessOperation,
1212
} from '@/lib/mothership/memories/application/resolve-access'
1313

1414
export const POST = defineInternalJsonRoute({
15-
contract: resolveMemoryAccessContract,
16-
auth: internalCopilotAuth(MEMORY_ACCESS_AUDIENCE, { organization: true }),
17-
operation: resolveMemoryAccessOperation,
15+
contract: resolveMemoryNotesAccessContract,
16+
auth: internalCopilotAuth(MEMORY_NOTES_ACCESS_AUDIENCE, { organization: true }),
17+
operation: resolveMemoryNotesAccessOperation,
1818
rateLimit: internalRateLimits.none({
1919
reason:
2020
'Private worker preflight rechecks live memory authority without reading or writing note content.',
2121
}),
2222
errorPolicy: internalOrchestrationErrorPolicy,
2323
mapInput: ({ body }) => body,
24-
useCase: resolveMemoryAccess,
24+
useCase: resolveMemoryNotesAccess,
2525
})
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { defineRouteContract } from '@/lib/api/contracts/types'
2-
import { MemoryAccessInput, MemoryAccessResult } from '@/lib/mothership/generated/memory-access'
2+
import {
3+
MemoryNotesAccessInput,
4+
MemoryNotesAccessResult,
5+
} from '@/lib/mothership/generated/memory-notes-access'
36

4-
export const resolveMemoryAccessContract = defineRouteContract({
7+
export const resolveMemoryNotesAccessContract = defineRouteContract({
58
method: 'POST',
69
path: '/api/mothership/memories/access',
7-
body: MemoryAccessInput,
8-
response: { mode: 'json', schema: MemoryAccessResult },
10+
body: MemoryNotesAccessInput,
11+
response: { mode: 'json', schema: MemoryNotesAccessResult },
912
})

apps/sim/lib/mothership/generated/memory-access.ts renamed to apps/sim/lib/mothership/generated/memory-notes-access.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// GENERATED — do not edit. Source of truth: mothership worker packages/contracts/src/memory-access.ts
1+
// GENERATED — do not edit. Source of truth: mothership worker packages/contracts/src/memory-notes-access.ts
22
// Regenerate with `bun run contracts:sync` in the worker.
33

44
import { z } from "zod";
@@ -15,17 +15,17 @@ const projectSchema = z.strictObject({
1515
permissions: permissionsSchema,
1616
});
1717

18-
export const MemoryAccessInput = z.strictObject({
18+
export const MemoryNotesAccessInput = z.strictObject({
1919
target: z.discriminatedUnion("scope", [
2020
z.strictObject({ scope: z.literal("personal") }),
2121
z.strictObject({ scope: z.literal("workspace"), workspaceId: z.uuid() }),
2222
z.strictObject({ scope: z.literal("project"), projectId: z.uuid() }),
2323
]),
2424
action: z.enum(["read", "private_mutation", "shared_mutation"]),
2525
});
26-
export type MemoryAccessInput = z.infer<typeof MemoryAccessInput>;
26+
export type MemoryNotesAccessInput = z.infer<typeof MemoryNotesAccessInput>;
2727

28-
export const MemoryAccessResult = z.strictObject({
28+
export const MemoryNotesAccessResult = z.strictObject({
2929
success: z.literal(true),
3030
version: z.literal(1),
3131
actor: z.strictObject({ userId: z.string().min(1), chatId: z.uuid() }),
@@ -44,7 +44,7 @@ export const MemoryAccessResult = z.strictObject({
4444
}),
4545
]),
4646
});
47-
export type MemoryAccessResult = z.infer<typeof MemoryAccessResult>;
47+
export type MemoryNotesAccessResult = z.infer<typeof MemoryNotesAccessResult>;
4848

4949
export const MemoryCleanupInput = z.strictObject({
5050
version: z.literal(1),

‎apps/sim/lib/mothership/generated/sim-transport.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { z } from "zod";
55
import { IntegrationCatalogRequest } from "./integration-catalog";
6-
import { MemoryAccessInput } from "./memory-access";
6+
import { MemoryNotesAccessInput } from "./memory-notes-access";
77
import { MemoryScopeRequest } from "./memory-scope";
88
import { RunControlRequest } from "./run-control";
99
import { TaskWakeRequest, WorkflowWatchRequest } from "./tasks";
@@ -30,7 +30,7 @@ export type SimScope = z.infer<typeof SimScope>;
3030

3131
/** Only idempotent controls use the idle channel; tool effects use the run event log. */
3232
export const SimControlOperation = z.discriminatedUnion("kind", [
33-
z.object({ kind: z.literal("memory_access"), input: MemoryAccessInput }),
33+
z.object({ kind: z.literal("memory_notes_access"), input: MemoryNotesAccessInput }),
3434
z.object({ kind: z.literal("memory_scope"), input: MemoryScopeRequest }),
3535
z.object({ kind: z.literal("run_control"), input: RunControlRequest }),
3636
z.object({ kind: z.literal("workflow_status"), input: WorkflowWatchRequest }),

‎apps/sim/lib/mothership/memories/application/resolve-access.integration.ts‎

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import {
2020
createTrustedCopilotPrincipal,
2121
createTrustedOrganizationCopilotPrincipal,
2222
} from '@/lib/mothership/auth/application-delegation'
23-
import { MemoryAccessResult } from '@/lib/mothership/generated/memory-access'
23+
import { MemoryNotesAccessResult } from '@/lib/mothership/generated/memory-notes-access'
2424
import {
25-
MEMORY_ACCESS_AUDIENCE,
26-
resolveMemoryAccess,
25+
MEMORY_NOTES_ACCESS_AUDIENCE,
26+
resolveMemoryNotesAccess,
2727
} from '@/lib/mothership/memories/application/resolve-access'
2828
import { executeSimControl } from '@/lib/mothership/transport/control'
2929
import { splitForkProject } from '@/lib/projects/membership'
@@ -45,7 +45,7 @@ const workspaceChatId = generateId()
4545
const organizationChatId = generateId()
4646

4747
function principal(organizationChat = false) {
48-
const options = { audience: MEMORY_ACCESS_AUDIENCE, ttlMs: 60_000 }
48+
const options = { audience: MEMORY_NOTES_ACCESS_AUDIENCE, ttlMs: 60_000 }
4949
return organizationChat
5050
? createTrustedOrganizationCopilotPrincipal(
5151
{ userId: actorId, organizationId, chatId: organizationChatId, delegationId: generateId() },
@@ -156,7 +156,7 @@ it('denies every Project memory action on both transports when projects is disab
156156
chatId,
157157
...(organizationChat ? { organizationId } : { workspaceId }),
158158
},
159-
operation: { kind: 'memory_access', input },
159+
operation: { kind: 'memory_notes_access', input },
160160
})
161161
expect(checkpoint.status).toBe(503)
162162
expect(JSON.parse(checkpoint.body)).toEqual({
@@ -170,19 +170,19 @@ it('keeps personal and workspace memories usable without Project context when pr
170170
featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(false)
171171
for (const organizationChat of [false, true]) {
172172
for (const action of ['read', 'private_mutation'] as const) {
173-
const personal = await resolveMemoryAccess.execute({
173+
const personal = await resolveMemoryNotesAccess.execute({
174174
principal: principal(organizationChat),
175175
input: { target: { scope: 'personal' }, action },
176176
})
177177
expect(personal.target).toEqual({
178178
scope: 'personal',
179179
permissions: { read: true, privateMutation: true, sharedMutation: false },
180180
})
181-
const result = await resolveMemoryAccess.execute({
181+
const result = await resolveMemoryNotesAccess.execute({
182182
principal: principal(organizationChat),
183183
input: { target: { scope: 'workspace', workspaceId }, action },
184184
})
185-
expect(MemoryAccessResult.parse(result).target).toEqual({
185+
expect(MemoryNotesAccessResult.parse(result).target).toEqual({
186186
scope: 'workspace',
187187
id: workspaceId,
188188
name: 'Environment',
@@ -191,7 +191,7 @@ it('keeps personal and workspace memories usable without Project context when pr
191191
})
192192
}
193193
await expect(
194-
resolveMemoryAccess.execute({
194+
resolveMemoryNotesAccess.execute({
195195
principal: principal(organizationChat),
196196
input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' },
197197
})
@@ -203,7 +203,7 @@ it('keeps personal and workspace memories usable without Project context when pr
203203
.where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId)))
204204
try {
205205
for (const organizationChat of [false, true]) {
206-
const result = await resolveMemoryAccess.execute({
206+
const result = await resolveMemoryNotesAccess.execute({
207207
principal: principal(organizationChat),
208208
input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' },
209209
})
@@ -222,7 +222,7 @@ it('keeps personal and workspace memories usable without Project context when pr
222222

223223
it('lets a read-only end user mutate private notes through either chat and keeps Project shared writes denied', async () => {
224224
for (const organizationChat of [false, true]) {
225-
const result = await resolveMemoryAccess.execute({
225+
const result = await resolveMemoryNotesAccess.execute({
226226
principal: principal(organizationChat),
227227
input: { target: { scope: 'workspace', workspaceId }, action: 'private_mutation' },
228228
})
@@ -234,7 +234,7 @@ it('lets a read-only end user mutate private notes through either chat and keeps
234234
currentProject: { id: projectId, permissions: { read: true, sharedMutation: false } },
235235
})
236236
await expect(
237-
resolveMemoryAccess.execute({
237+
resolveMemoryNotesAccess.execute({
238238
principal: principal(organizationChat),
239239
input: { target: { scope: 'project', projectId }, action: 'shared_mutation' },
240240
})
@@ -248,7 +248,7 @@ it('does not turn write in one environment into shared Project write', async ()
248248
.set({ permissionType: 'write' })
249249
.where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId)))
250250
try {
251-
const result = await resolveMemoryAccess.execute({
251+
const result = await resolveMemoryNotesAccess.execute({
252252
principal: principal(),
253253
input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' },
254254
})
@@ -263,7 +263,7 @@ it('does not turn write in one environment into shared Project write', async ()
263263
entityId: siblingId,
264264
permissionType: 'write',
265265
})
266-
const shared = await resolveMemoryAccess.execute({
266+
const shared = await resolveMemoryNotesAccess.execute({
267267
principal: principal(true),
268268
input: { target: { scope: 'project', projectId }, action: 'shared_mutation' },
269269
})
@@ -286,11 +286,11 @@ it('rejects wrong actor, audience, expired delegation and another workspace targ
286286
{ ...principal(), audience: 'sim:workspaces' },
287287
{ ...principal(), expiresAt: new Date(0) },
288288
])
289-
await expect(resolveMemoryAccess.execute({ principal: altered, input })).rejects.toBeInstanceOf(
290-
Error
291-
)
289+
await expect(
290+
resolveMemoryNotesAccess.execute({ principal: altered, input })
291+
).rejects.toBeInstanceOf(Error)
292292
await expect(
293-
resolveMemoryAccess.execute({
293+
resolveMemoryNotesAccess.execute({
294294
principal: principal(),
295295
input: { ...input, target: { scope: 'workspace', workspaceId: siblingId } },
296296
})
@@ -303,12 +303,12 @@ it('denies a lost scoped grant while personal-only organization context remains
303303
.where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId)))
304304
try {
305305
await expect(
306-
resolveMemoryAccess.execute({
306+
resolveMemoryNotesAccess.execute({
307307
principal: principal(true),
308308
input: { target: { scope: 'project', projectId }, action: 'private_mutation' },
309309
})
310310
).rejects.toMatchObject({ code: 'not_found' })
311-
const result = await resolveMemoryAccess.execute({
311+
const result = await resolveMemoryNotesAccess.execute({
312312
principal: principal(true),
313313
input: { target: { scope: 'personal' }, action: 'private_mutation' },
314314
})
@@ -353,7 +353,7 @@ it('applies the Files capability to shared Project writes without blocking priva
353353
organizationId,
354354
})
355355
try {
356-
const result = await resolveMemoryAccess.execute({
356+
const result = await resolveMemoryNotesAccess.execute({
357357
principal: principal(true),
358358
input: { target: { scope: 'project', projectId }, action: 'private_mutation' },
359359
})
@@ -363,7 +363,7 @@ it('applies the Files capability to shared Project writes without blocking priva
363363
sharedMutation: false,
364364
})
365365
await expect(
366-
resolveMemoryAccess.execute({
366+
resolveMemoryNotesAccess.execute({
367367
principal: principal(true),
368368
input: { target: { scope: 'project', projectId }, action: 'shared_mutation' },
369369
})
@@ -393,13 +393,13 @@ it('rejects cross-organization authority and organization non-agent mode', async
393393
try {
394394
const otherPrincipal = { ...principal(true), organizationId: otherId }
395395
await expect(
396-
resolveMemoryAccess.execute({
396+
resolveMemoryNotesAccess.execute({
397397
principal: otherPrincipal,
398398
input: { target: { scope: 'project', projectId }, action: 'read' },
399399
})
400400
).rejects.toMatchObject({ code: 'not_found' })
401401
await expect(
402-
resolveMemoryAccess.execute({
402+
resolveMemoryNotesAccess.execute({
403403
principal: otherPrincipal,
404404
input: { target: { scope: 'workspace', workspaceId }, action: 'read' },
405405
})
@@ -418,7 +418,7 @@ it('rejects cross-organization authority and organization non-agent mode', async
418418
.where(eq(copilotChats.id, organizationChatId))
419419
try {
420420
await expect(
421-
resolveMemoryAccess.execute({
421+
resolveMemoryNotesAccess.execute({
422422
principal: principal(true),
423423
input: { target: { scope: 'project', projectId }, action: 'read' },
424424
})
@@ -457,7 +457,7 @@ it('serves the typed route using authenticated headers and rejects body identity
457457
)
458458
const response = await POST(request(input))
459459
expect(response.status).toBe(200)
460-
expect(MemoryAccessResult.parse(await response.json())).toMatchObject({
460+
expect(MemoryNotesAccessResult.parse(await response.json())).toMatchObject({
461461
success: true,
462462
version: 1,
463463
actor: { userId: actorId, chatId: workspaceChatId },
@@ -476,13 +476,13 @@ it('resolves detach to the new Project and rejects the former Project from the w
476476
return id
477477
})
478478
try {
479-
const result = await resolveMemoryAccess.execute({
479+
const result = await resolveMemoryNotesAccess.execute({
480480
principal: principal(),
481481
input: { target: { scope: 'workspace', workspaceId }, action: 'read' },
482482
})
483483
expect(result.target).toMatchObject({ currentProject: { id: detachedProject } })
484484
await expect(
485-
resolveMemoryAccess.execute({
485+
resolveMemoryNotesAccess.execute({
486486
principal: principal(),
487487
input: { target: { scope: 'project', projectId }, action: 'read' },
488488
})
@@ -515,12 +515,12 @@ it('uses identical current authority over checkpoint transport for workspace and
515515
expiresAt: Date.now() + 60_000,
516516
scope,
517517
operation: {
518-
kind: 'memory_access',
518+
kind: 'memory_notes_access',
519519
input: { target: { scope: 'workspace', workspaceId }, action: 'private_mutation' },
520520
},
521521
})
522522
expect(result.status).toBe(200)
523-
expect(MemoryAccessResult.parse(JSON.parse(result.body))).toMatchObject({
523+
expect(MemoryNotesAccessResult.parse(JSON.parse(result.body))).toMatchObject({
524524
actor: { userId: actorId, chatId: scope.chatId },
525525
target: { id: workspaceId, currentProject: projectsEnabled ? { id: projectId } : null },
526526
})
@@ -529,7 +529,7 @@ it('uses identical current authority over checkpoint transport for workspace and
529529
expiresAt: Date.now() + 60_000,
530530
scope,
531531
operation: {
532-
kind: 'memory_access',
532+
kind: 'memory_notes_access',
533533
input: { target: { scope: 'project', projectId }, action: 'shared_mutation' },
534534
},
535535
})

0 commit comments

Comments
 (0)