import { IBootstrapService, IHostFileSystem, IWorkspaceInstanceManager, IWorkspaceService, IWorkspaceSessions, IWorkspaceTrust, type Scope, type Workspace, } from '@moonshot-ai/agent-core-v2'; import { isAbsolute, join, normalize, resolve } from 'node:path'; import { z } from 'zod'; import { errEnvelope, okEnvelope } from '../envelope'; import { requestLog } from '../lib/requestLog'; import { defineRoute } from '../middleware/defineRoute'; import { ErrorCode } from '../protocol/error-codes'; import { addDirRequestSchema, addDirResponseSchema, createWorkspaceRequestSchema, createWorkspaceResponseSchema, deleteWorkspaceResponseSchema, listWorkspacesResponseSchema, updateWorkspaceRequestSchema, updateWorkspaceResponseSchema, workspaceIdParamSchema, workspaceTrustResponseSchema, } from '../protocol/rest-workspace'; import type { Workspace as WorkspaceWire } from '../protocol/workspace'; interface WorkspaceRouteHost { get( path: string, options: { preHandler: unknown[]; schema?: Record } | undefined, handler: ( req: { id: string }, reply: { send(payload: unknown): unknown }, ) => Promise | void, ): unknown; post( path: string, options: { preHandler: unknown[]; schema?: Record }, handler: ( req: { id: string; body: unknown; params: unknown }, reply: { send(payload: unknown): unknown }, ) => Promise | void, ): unknown; patch( path: string, options: { preHandler: unknown[]; schema?: Record }, handler: ( req: { id: string; body: unknown; params: unknown }, reply: { send(payload: unknown): unknown }, ) => Promise | void, ): unknown; delete( path: string, options: { preHandler: unknown[]; schema?: Record } | undefined, handler: ( req: { id: string; params: unknown }, reply: { send(payload: unknown): unknown }, ) => Promise | void, ): unknown; } const detailsSchema = z.array(z.object({ path: z.string(), message: z.string() })); export function registerWorkspacesRoutes(app: WorkspaceRouteHost, core: Scope): void { const listRoute = defineRoute( { method: 'GET', path: '/workspaces', success: { data: listWorkspacesResponseSchema }, description: 'List registered workspaces', tags: ['workspaces'], }, async (req, reply) => { const items = await core.accessor.get(IWorkspaceService).list(); const projected = await Promise.all(items.map((ws) => toWireWorkspace(core, ws))); reply.send(okEnvelope({ items: projected }, req.id)); }, ); app.get(listRoute.path, listRoute.options, listRoute.handler as Parameters[2]); const createRoute = defineRoute( { method: 'POST', path: '/workspaces', body: createWorkspaceRequestSchema, success: { data: createWorkspaceResponseSchema }, errors: { [ErrorCode.VALIDATION_FAILED]: { detailsSchema }, [ErrorCode.FS_PATH_NOT_FOUND]: {}, }, description: 'Register a workspace (idempotent on root)', tags: ['workspaces'], }, async (req, reply) => { const root = req.body.root; if (!isAbsolute(root)) { reply.send( buildValidationEnvelope( [{ path: 'root', message: 'root must be an absolute path' }], req.id, ), ); return; } const hostFs = core.accessor.get(IHostFileSystem); try { const stat = await hostFs.stat(root); if (!stat.isDirectory) { reply.send( errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, `root ${root} is not a directory`, req.id), ); return; } } catch { reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, `root ${root} does not exist`, req.id)); return; } const ws = await core.accessor.get(IWorkspaceService).createOrTouch(root, req.body.name); reply.send(okEnvelope(await toWireWorkspace(core, ws), req.id)); }, ); app.post( createRoute.path, createRoute.options, createRoute.handler as Parameters[2], ); const updateRoute = defineRoute( { method: 'PATCH', path: '/workspaces/{workspace_id}', params: workspaceIdParamSchema, body: updateWorkspaceRequestSchema, success: { data: updateWorkspaceResponseSchema }, errors: { [ErrorCode.VALIDATION_FAILED]: { detailsSchema }, [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Rename a workspace (display name only)', tags: ['workspaces'], }, async (req, reply) => { const { workspace_id } = req.params; const ws = await core.accessor .get(IWorkspaceService) .update(workspace_id, { name: req.body.name }); if (ws === undefined) { reply.send( errEnvelope(ErrorCode.WORKSPACE_NOT_FOUND, `workspace ${workspace_id} does not exist`, req.id), ); return; } reply.send(okEnvelope(await toWireWorkspace(core, ws), req.id)); }, ); app.patch( updateRoute.path, updateRoute.options, updateRoute.handler as Parameters[2], ); const deleteRoute = defineRoute( { method: 'DELETE', path: '/workspaces/{workspace_id}', params: workspaceIdParamSchema, success: { data: deleteWorkspaceResponseSchema }, errors: { [ErrorCode.VALIDATION_FAILED]: { detailsSchema }, [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Unregister a workspace (does not remove on-disk content)', tags: ['workspaces'], }, async (req, reply) => { const { workspace_id } = req.params; const registry = core.accessor.get(IWorkspaceService); const existing = await registry.get(workspace_id); if (existing === undefined) { reply.send( errEnvelope(ErrorCode.WORKSPACE_NOT_FOUND, `workspace ${workspace_id} does not exist`, req.id), ); return; } await registry.delete(workspace_id); requestLog(req)?.info({ workspace_id }, 'workspace deleted'); reply.send(okEnvelope({ deleted: true as const }, req.id)); }, ); app.delete( deleteRoute.path, deleteRoute.options, deleteRoute.handler as Parameters[2], ); const getTrustRoute = defineRoute( { method: 'GET', path: '/workspaces/{workspace_id}/trust', params: workspaceIdParamSchema, success: { data: workspaceTrustResponseSchema }, errors: { [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Read the workspace trust state', tags: ['workspaces'], }, async (req, reply) => { const trust = await resolveTrust(core, req.params.workspace_id, req.id, reply); if (trust === undefined) return; reply.send(okEnvelope({ trusted: await trust.get() }, req.id)); }, ); app.get( getTrustRoute.path, getTrustRoute.options, getTrustRoute.handler as Parameters[2], ); const trustRoute = defineRoute( { method: 'POST', path: '/workspaces/{workspace_id}/trust', params: workspaceIdParamSchema, success: { data: workspaceTrustResponseSchema }, errors: { [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Mark the workspace trusted (project-level MCP config loads)', tags: ['workspaces'], }, async (req, reply) => { const trust = await resolveTrust(core, req.params.workspace_id, req.id, reply); if (trust === undefined) return; await trust.trust(); reply.send(okEnvelope({ trusted: true }, req.id)); }, ); app.post( trustRoute.path, trustRoute.options, trustRoute.handler as Parameters[2], ); const untrustRoute = defineRoute( { method: 'POST', path: '/workspaces/{workspace_id}/untrust', params: workspaceIdParamSchema, success: { data: workspaceTrustResponseSchema }, errors: { [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Revoke workspace trust (project-level MCP config unloads)', tags: ['workspaces'], }, async (req, reply) => { const trust = await resolveTrust(core, req.params.workspace_id, req.id, reply); if (trust === undefined) return; await trust.untrust(); reply.send(okEnvelope({ trusted: false }, req.id)); }, ); app.post( untrustRoute.path, untrustRoute.options, untrustRoute.handler as Parameters[2], ); const addDirRoute = defineRoute( { method: 'POST', path: '/workspaces/{workspace_id}/add-dir', params: workspaceIdParamSchema, body: addDirRequestSchema, success: { data: addDirResponseSchema }, errors: { [ErrorCode.VALIDATION_FAILED]: { detailsSchema }, [ErrorCode.FS_PATH_NOT_FOUND]: {}, [ErrorCode.WORKSPACE_NOT_FOUND]: {}, }, description: 'Add an additional directory to the workspace', tags: ['workspaces'], }, async (req, reply) => { const { workspace_id } = req.params; const ws = await core.accessor.get(IWorkspaceService).get(workspace_id); if (ws === undefined) { reply.send( errEnvelope(ErrorCode.WORKSPACE_NOT_FOUND, `workspace ${workspace_id} does not exist`, req.id), ); return; } const resolved = resolveAdditionalDirPath(core, ws.root, req.body.path); const hostFs = core.accessor.get(IHostFileSystem); try { const stat = await hostFs.stat(resolved); if (!stat.isDirectory) { reply.send( errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, `path ${req.body.path} is not a directory`, req.id), ); return; } } catch { reply.send( errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, `path ${req.body.path} does not exist`, req.id), ); return; } const workspace = await core .accessor.get(IWorkspaceInstanceManager) .getOrCreate({ workspaceId: workspace_id, root: ws.root }); const result = await workspace.program.dirs.addDir({ path: req.body.path, persist: req.body.persist, }); reply.send( okEnvelope( { project_root: result.projectRoot, config_path: result.configPath, additional_dirs: [...result.additionalDirs], persisted: result.persisted, }, req.id, ), ); }, ); app.post( addDirRoute.path, addDirRoute.options, addDirRoute.handler as Parameters[2], ); } function resolveAdditionalDirPath(core: Scope, root: string, input: string): string { const trimmed = input.trim(); const osHomeDir = core.accessor.get(IBootstrapService).osHomeDir; const expanded = trimmed === '~' ? osHomeDir : trimmed.startsWith('~/') ? join(osHomeDir, trimmed.slice(2)) : trimmed; return isAbsolute(expanded) ? normalize(expanded) : resolve(root, expanded); } type TrustReply = { send(payload: unknown): unknown }; async function resolveTrust( core: Scope, workspaceId: string, requestId: string, reply: TrustReply, ): Promise { const ws = await core.accessor.get(IWorkspaceService).get(workspaceId); if (ws === undefined) { reply.send( errEnvelope(ErrorCode.WORKSPACE_NOT_FOUND, `workspace ${workspaceId} does not exist`, requestId), ); return undefined; } const workspace = await core .accessor.get(IWorkspaceInstanceManager) .getOrCreate({ workspaceId, root: ws.root }); return workspace.program.trust; } export async function toWireWorkspace(core: Scope, ws: Workspace): Promise { const sessionCount = await core.accessor.get(IWorkspaceSessions).count(ws.id); return { id: ws.id, root: ws.root, name: ws.name, created_at: new Date(ws.createdAt).toISOString(), last_opened_at: new Date(ws.lastOpenedAt).toISOString(), session_count: sessionCount, }; } function buildValidationEnvelope( details: { path: string; message: string }[], requestId: string, ): { code: number; msg: string; data: null; request_id: string; details: { path: string; message: string }[]; } { const first = details[0]; const msg = first === undefined ? 'validation failed' : `${first.path}: ${first.message}`; return { code: ErrorCode.VALIDATION_FAILED, msg, data: null, request_id: requestId, details, }; }