feat(sharing): project shares, co-edit safety, awareness UI (Phase 4c+d+e)
Adds project-level sharing via the new project_shares table plus the
infrastructure that makes multi-user editing safe and visible.
Authorization (lib/access.ts):
- getAccessibleProjects / getProjectAccess centralise the predicate
used by every read and write path.
- readableProjectIds / writableProjectIds drive listing-style queries.
- Web UI Server Actions and pages source group memberships from the
user_groups table so authorization works without depending on
Agent A's session callback shape.
Optimistic locking:
- memories + snippets gain version + last_edited_by columns. Every
UPDATE bumps version and stamps the editor; UPDATE WHERE clauses
require the caller's pre-fetched version, surfacing a clear
"refresh and try again" error on lost-write races rather than
silently clobbering.
- MemoryUpdateInput / SnippetPutInput accept an optional version
token.
MCP tools:
- memory.write / .update / .delete / .get / .list / .search,
snippet.put / .get / .list / .delete now respect shared-project
access (read = owner | any share, write = owner | rw share).
- project, defaults to ctx.defaultProjectKey from the X-Project-Key
header (populated by the MCP route — Agent A's wiring).
- project.identify returns shared projects you have access to and
prefers an owned project on key collision, audit-logging the
collision so an operator can debug it.
- Tool descriptions for memory.update, memory.write, snippet.put,
and project.identify updated with the co-edit / shared-project
notes.
Web UI:
- Project detail page: ownership badge, shared-with-N-groups badge,
owner-only "Manage sharing" section (add/flip/remove shares via
lib/share-actions.ts). Add-share is constrained to groups the
granter is already in.
- "Shared" chips on memory cards in /memories and /dashboard.
- "Last edited by ..." on memory + snippet detail pages, shown only
when the last editor isn't the row's original author so the chip
stays informative.
- Read-only viewers (ro shares) lose Edit/Delete affordances on
memories and snippets.
Migration 0004_project_shares.sql adds project_shares + the two new
columns on memories and snippets; it depends on Agent A's
0003_groups.sql for the groups, user_groups, and memory_access enum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,11 @@ export type MemoryScope = z.infer<typeof MemoryScope>;
|
||||
export const MemoryVisibility = z.enum(["private", "shared", "team"]);
|
||||
export type MemoryVisibility = z.infer<typeof MemoryVisibility>;
|
||||
|
||||
// Access level a group has on a shared project. Mirrors the Postgres
|
||||
// `memory_access` enum defined by Agent A's groups migration.
|
||||
export const MemoryAccess = z.enum(["ro", "rw"]);
|
||||
export type MemoryAccess = z.infer<typeof MemoryAccess>;
|
||||
|
||||
export const ProjectKey = z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -48,6 +53,10 @@ export const MemoryUpdateInput = z.object({
|
||||
tags: Tags.optional(),
|
||||
scope: MemoryScope.optional(),
|
||||
project: ProjectKey.optional(),
|
||||
// Optimistic-locking token returned by memory.get / memory.list. When
|
||||
// present, the UPDATE matches on (id, version); a 0-row result means
|
||||
// someone else edited this memory since you read it.
|
||||
version: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
@@ -123,6 +132,9 @@ export const SnippetPutInput = z
|
||||
tags: Tags.optional(),
|
||||
scope: MemoryScope.default("user"),
|
||||
project: ProjectKey.optional(),
|
||||
// Optimistic-locking token used on the update path (when a row with
|
||||
// this name+scope+project already exists). Ignored on first put.
|
||||
version: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.refine(scopeProjectRefinement.check, { message: scopeProjectRefinement.message });
|
||||
export type SnippetPutInput = z.infer<typeof SnippetPutInput>;
|
||||
|
||||
Reference in New Issue
Block a user