Repository navigation
feat: shared folders - #137
maxy-shpfy wants to merge 1 commit into
Conversation
acc1bc7 to
b26db1e
Compare
a29a3e7 to
9165713
Compare
9165713 to
ce3c98f
Compare
| const uri = `mount://${folder.id}`; | ||
| const resources = await deps.catalog.listForSession(sessionId); | ||
| const existing = resources.find((resource) => resource.uri === uri); | ||
| const mountName = |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
Remote resource.add bypasses the mount-name guard (path traversal)
The only single-segment check on an explicit mountName is the zod refine in routes/sessions/schemas.ts, but RemoteEnvironmentGateway.handleResourceAdd passes request.input straight to applyResourceInput without hostResourceInputSchema (unlike externalAgentHandlers.ts). A remote environment authorized for the session can send mountName: "../<otherSessionId>/x" or "artifacts/x", and path.join(rootPath, mountName) in createMountSymlink plants a symlink outside the session root or inside a reserved / HTTP-served subtree — the reserved-name check is exact-match only.
The file kind has an authoritative server-side guard (resolveWithinUploads); suggest the same here:
if (
mountName !== path.basename(mountName) ||
mountName === "." ||
mountName === ".." ||
mountName.includes("\\")
) {
throw badRequest("Mount name must be a single path segment");
}and optionally hostResourceInputSchema.safeParse(request.input) in the gateway.
| ): CatalogInput { | ||
| return { | ||
| sessionId, | ||
| kind: "mount", |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
Mounts aren't surfaced to agents
renderHostResourcesPreamble and /internal/resources both filter to kind === "host", so Prime never learns that <mountName>/ exists, that it's shared with other sessions, or that edits are live. Consider listing mount entries (name → path, plus a "shared across sessions, no locking" note) in the preamble and in read_resources — or note in the PR if that's a planned follow-up.
| * to, so edits in one session are visible to the others. Gitignored by default. | ||
| */ | ||
| export const SHARED_ROOT = | ||
| process.env.SHARED_ROOT ?? path.resolve(process.cwd(), ".shared"); |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
SHARED_ROOT isn't wired into pnpm dev / turbo
The README now says pnpm dev sets .shared, but the root dev script doesn't set SHARED_ROOT, so it falls back to apps/server/.shared (the other roots live at the repo root). It's also missing from globalPassThroughEnv in turbo.json, so with turbo's strict env mode a SHARED_ROOT set in the shell is dropped under pnpm dev.
Suggest adding SHARED_ROOT="$PWD/.shared" to the dev script and SHARED_ROOT to globalPassThroughEnv (#145's QMD_STATE_ROOT needs the same). Deployments should also set it on persistent storage alongside SESSIONS_ROOT; otherwise the shared_folders rows survive a redeploy while the folders don't, leaving dangling mount symlinks.
| const resources = await deps.catalog.listForSession(sessionId); | ||
| const existing = resources.find((resource) => resource.uri === uri); | ||
| const mountName = | ||
| (existing?.meta?.mountName as string | undefined) ?? |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
Unsafe cast on the stored mount name
meta is untyped catalog data; a type guard avoids feeding a non-string into path.join:
const stored = existing?.meta?.mountName;
const mountName =
(typeof stored === "string" ? stored : undefined) ??
requestedName ??
sanitizeMountName(folder.name);|
|
||
| /** Body for `POST /api/shared-folders`. */ | ||
| const createSharedFolderSchema = z.object({ | ||
| name: z.string().optional(), |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
Shared-folder name is unbounded
The name becomes the default mount name via sanitizeMountName, which doesn't strip NUL or cap length, so a very long name (or one containing \0) fails later in fs.symlink as a 500 at mount time rather than a 400 here. Suggest:
name: z
.string()
.trim()
.min(1)
.max(128)
.refine((name) => !name.includes("\0"))
.optional(),plus the same .max() on the mountName schema in routes/sessions/schemas.ts.
Description
Adds shared folders: server-managed "project" directories that live outside any single
session so multiple sessions can collaborate in one live workspace.
SHARED_ROOT/<id>directory; metadata is tracked in a newshared_foldersSQLite table (migration0024) with aSharedFolderStore(SQLite + in-memory implementations).
/api/shared-folders:GET /api/shared-folders— list every shared folder (oldest first).POST /api/shared-folders— create a folder and its backing directory; the creator isresolved from their identity when available, and the name defaults to
Shared folder N.mounthost-resource kind. A mount creates aone-way symlink at the session root pointing at the shared folder, so the agent reads and
writes it with its ordinary file tools.
mountNameis an optional single-segment symlinkname (defaults to a sanitized form of the folder's name).
no unmount.
SharedFolderStorethrough the resource routes andseed deps.
Contracts (
@tangent/shared), the embed types, and server docs/README are updated todescribe the feature and the new
SHARED_ROOTconfig.Screenshots
N/A
How to test
pnpm --filter @tangent/server db:migrateto apply migration0024.pnpm dev(local run pointsSHARED_ROOTat a gitignored.sharedfolder).POST /api/shared-folderswith an optional{ "name": "..." }and confirm a201with thecreated folder plus a backing directory under
SHARED_ROOT/<id>.GET /api/shared-foldersand confirm it lists the folder.mounthost resource to a session referencing thefolderId; confirm a symlinkappears at the session root and that two sessions mounting the same folder see each other's
writes live.
pnpm --filter @tangent/server exec tsx --test src/store/sqliteSharedFolderStore.test.tspnpm --filter @tangent/server exec tsx --test src/routes/sessions/resources.test.tsNotes
SHARED_ROOTenv var — set it in deployed environments;pnpm devdefaults to.shared.Stack created with GitHub Stacks CLI • Give Feedback 💬