kimi-code / packages /kap-server /src /routes /workspaces.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw History Blame Contribute Delete
12.8 kB
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<string, unknown> } | undefined,
handler: (
req: { id: string },
reply: { send(payload: unknown): unknown },
) => Promise<void> | void,
): unknown;
post(
path: string,
options: { preHandler: unknown[]; schema?: Record<string, unknown> },
handler: (
req: { id: string; body: unknown; params: unknown },
reply: { send(payload: unknown): unknown },
) => Promise<void> | void,
): unknown;
patch(
path: string,
options: { preHandler: unknown[]; schema?: Record<string, unknown> },
handler: (
req: { id: string; body: unknown; params: unknown },
reply: { send(payload: unknown): unknown },
) => Promise<void> | void,
): unknown;
delete(
path: string,
options: { preHandler: unknown[]; schema?: Record<string, unknown> } | undefined,
handler: (
req: { id: string; params: unknown },
reply: { send(payload: unknown): unknown },
) => Promise<void> | 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<WorkspaceRouteHost['get']>[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<WorkspaceRouteHost['post']>[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<WorkspaceRouteHost['patch']>[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<WorkspaceRouteHost['delete']>[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<WorkspaceRouteHost['get']>[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<WorkspaceRouteHost['post']>[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<WorkspaceRouteHost['post']>[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<WorkspaceRouteHost['post']>[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<IWorkspaceTrust | undefined> {
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<WorkspaceWire> {
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,
};
}