@cospacehq/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/dist/agent-runtime.d.ts +45 -0
  4. package/dist/agent-runtime.d.ts.map +1 -0
  5. package/dist/agent-runtime.js +374 -0
  6. package/dist/agent-runtime.js.map +1 -0
  7. package/dist/config.d.ts +8 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +14 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/db.d.ts +128 -0
  12. package/dist/db.d.ts.map +1 -0
  13. package/dist/db.js +854 -0
  14. package/dist/db.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +34 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/model-client.d.ts +43 -0
  20. package/dist/model-client.d.ts.map +1 -0
  21. package/dist/model-client.js +138 -0
  22. package/dist/model-client.js.map +1 -0
  23. package/dist/provider-crypto.d.ts +4 -0
  24. package/dist/provider-crypto.d.ts.map +1 -0
  25. package/dist/provider-crypto.js +30 -0
  26. package/dist/provider-crypto.js.map +1 -0
  27. package/dist/sandbox.d.ts +3 -0
  28. package/dist/sandbox.d.ts.map +1 -0
  29. package/dist/sandbox.js +22 -0
  30. package/dist/sandbox.js.map +1 -0
  31. package/dist/server.d.ts +15 -0
  32. package/dist/server.d.ts.map +1 -0
  33. package/dist/server.js +1296 -0
  34. package/dist/server.js.map +1 -0
  35. package/package.json +33 -0
  36. package/src/agent-runtime.ts +479 -0
  37. package/src/config.ts +21 -0
  38. package/src/db.ts +1197 -0
  39. package/src/index.ts +44 -0
  40. package/src/model-client.ts +187 -0
  41. package/src/provider-crypto.ts +39 -0
  42. package/src/sandbox.ts +26 -0
  43. package/src/server.ts +1548 -0
  44. package/tsconfig.json +8 -0
package/dist/server.js ADDED
@@ -0,0 +1,1296 @@
1
+ import fs from "node:fs/promises";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import { URL } from "node:url";
5
+ import Fastify from "fastify";
6
+ import cors from "@fastify/cors";
7
+ import { WebSocketServer } from "ws";
8
+ import { AgentCreateInputSchema, AgentBindingUpdateInputSchema, BootstrapPayloadSchema, ClientMessageInputSchema, FileDeleteInputSchema, FileListInputSchema, FileMkdirInputSchema, FileRenameInputSchema, FileReadInputSchema, FileWriteInputSchema, ProjectAgentAssignmentInputSchema, ProjectCreateInputSchema, ProviderConnectionTestInputSchema, TaskCreateInputSchema, TaskStatusUpdateInputSchema, ProviderUpsertInputSchema } from "@cospacehq/shared";
9
+ import { AgentRuntime } from "./agent-runtime.js";
10
+ import { resolveConfig } from "./config.js";
11
+ import { CoSpaceStore } from "./db.js";
12
+ import { OpenAICompatibleModelClient } from "./model-client.js";
13
+ import { decryptProviderSecret, encryptProviderSecret, encryptionSecretFromToken } from "./provider-crypto.js";
14
+ import { listSandboxFiles, resolveSandboxPath } from "./sandbox.js";
15
+ function readTokenFromRequest(urlPath) {
16
+ const parsed = new URL(urlPath, "http://127.0.0.1");
17
+ return parsed.searchParams.get("token");
18
+ }
19
+ function readPasscodeFromRequest(urlPath) {
20
+ const parsed = new URL(urlPath, "http://127.0.0.1");
21
+ return parsed.searchParams.get("passcode");
22
+ }
23
+ function passcodeHashFromRaw(rawPasscode) {
24
+ return crypto.createHash("sha256").update(rawPasscode, "utf8").digest("hex");
25
+ }
26
+ function eventEnvelope(event) {
27
+ return JSON.stringify(event);
28
+ }
29
+ function maybeWebDist(webDistPath) {
30
+ if (!webDistPath) {
31
+ return null;
32
+ }
33
+ return path.resolve(webDistPath);
34
+ }
35
+ function mimeTypeForStaticFile(filePath) {
36
+ switch (path.extname(filePath).toLowerCase()) {
37
+ case ".css":
38
+ return "text/css; charset=utf-8";
39
+ case ".html":
40
+ return "text/html; charset=utf-8";
41
+ case ".js":
42
+ case ".mjs":
43
+ return "text/javascript; charset=utf-8";
44
+ case ".json":
45
+ case ".map":
46
+ return "application/json; charset=utf-8";
47
+ case ".ico":
48
+ return "image/x-icon";
49
+ case ".jpeg":
50
+ case ".jpg":
51
+ return "image/jpeg";
52
+ case ".png":
53
+ return "image/png";
54
+ case ".svg":
55
+ return "image/svg+xml";
56
+ case ".txt":
57
+ return "text/plain; charset=utf-8";
58
+ case ".woff":
59
+ return "font/woff";
60
+ case ".woff2":
61
+ return "font/woff2";
62
+ default:
63
+ return null;
64
+ }
65
+ }
66
+ function sanitizeProviderKey(rawValue) {
67
+ const trimmed = rawValue.trim();
68
+ if (!/^[a-zA-Z0-9._-]{2,64}$/.test(trimmed)) {
69
+ throw new Error("Provider key must be 2-64 chars and contain only letters, numbers, ., _, -");
70
+ }
71
+ return trimmed;
72
+ }
73
+ function mapProviderRecordToPublic(record) {
74
+ return {
75
+ id: record.id,
76
+ providerKey: record.providerKey,
77
+ label: record.label,
78
+ kind: record.kind,
79
+ baseUrl: record.baseUrl,
80
+ defaultModel: record.defaultModel,
81
+ hasApiKey: Boolean(record.encryptedApiKey),
82
+ createdAt: record.createdAt,
83
+ updatedAt: record.updatedAt
84
+ };
85
+ }
86
+ function toProviderRuntimeConfig(record, encryptionSecret, overrideApiKey, overrideBaseUrl, overrideKind) {
87
+ const apiKey = overrideApiKey ?? (record.encryptedApiKey ? decryptProviderSecret(record.encryptedApiKey, encryptionSecret) : null);
88
+ if (!apiKey) {
89
+ throw new Error(`Provider ${record.providerKey} does not have an API key configured`);
90
+ }
91
+ return {
92
+ providerKey: record.providerKey,
93
+ label: record.label,
94
+ kind: overrideKind ?? record.kind,
95
+ baseUrl: overrideBaseUrl ?? record.baseUrl,
96
+ apiKey
97
+ };
98
+ }
99
+ export async function startCoSpaceServer(options) {
100
+ const resolvedConfig = resolveConfig(options.config, options.configPath);
101
+ await fs.mkdir(resolvedConfig.workspaceRoot, { recursive: true });
102
+ await fs.mkdir(resolvedConfig.dataDir, { recursive: true });
103
+ const app = Fastify({ logger: true });
104
+ await app.register(cors, { origin: true });
105
+ const store = new CoSpaceStore(path.join(resolvedConfig.dataDir, "cospace.db"), {
106
+ projectName: resolvedConfig.projectName
107
+ });
108
+ let sessionToken = resolvedConfig.auth.token;
109
+ let sessionPasscodeHash = resolvedConfig.auth.passcodeHash ?? null;
110
+ const sockets = new Set();
111
+ const wss = new WebSocketServer({ noServer: true });
112
+ const emit = (event) => {
113
+ const payload = eventEnvelope(event);
114
+ for (const socket of sockets) {
115
+ if (socket.readyState === socket.OPEN) {
116
+ socket.send(payload);
117
+ }
118
+ }
119
+ };
120
+ const emitTaskUpdated = (task) => {
121
+ emit({ type: "task.updated", data: task });
122
+ };
123
+ const emitTaskEvent = (event) => {
124
+ emit({ type: "task.event", data: event });
125
+ };
126
+ const recordAndEmitTaskEvent = (input) => {
127
+ const event = store.recordTaskEvent(input);
128
+ emitTaskEvent(event);
129
+ };
130
+ const emitTaskMessage = (task, body, trace) => {
131
+ const assignee = task.assigneeAgentId ? store.getAgentById(task.assigneeAgentId) : null;
132
+ const senderId = assignee?.id ?? "task-system";
133
+ const senderName = assignee ? `${assignee.name} (${assignee.title})` : "Task System";
134
+ const internalTrace = assignee?.traceEnabled ? trace : null;
135
+ const message = store.createAgentMessage({
136
+ projectId: task.projectId,
137
+ senderId,
138
+ senderName,
139
+ body,
140
+ internalTrace
141
+ });
142
+ emit({ type: "message.created", data: message });
143
+ if (assignee?.seenEnabled) {
144
+ store.createSeenReceipt({
145
+ messageId: message.id,
146
+ agentId: assignee.id,
147
+ agentName: assignee.name,
148
+ model: assignee.model
149
+ });
150
+ emit({
151
+ type: "seen.updated",
152
+ data: {
153
+ messageId: message.id,
154
+ receipts: store.listReceiptsForMessage(message.id)
155
+ }
156
+ });
157
+ }
158
+ };
159
+ const taskActorId = (task) => task.assigneeAgentId ?? "task-system";
160
+ const taskPathFromPayload = (task) => {
161
+ const raw = task.actionPayload?.path?.trim() ?? null;
162
+ return raw && raw.length > 0 ? raw : null;
163
+ };
164
+ const normalizePolicyPath = (value) => {
165
+ if (!value) {
166
+ return null;
167
+ }
168
+ return value.replace(/^\/+/g, "");
169
+ };
170
+ const policyPrefixMatch = (targetPath, prefixes) => {
171
+ const normalized = normalizePolicyPath(targetPath);
172
+ if (!normalized) {
173
+ return false;
174
+ }
175
+ return prefixes.some((prefix) => normalized.startsWith(prefix));
176
+ };
177
+ const resolveApprovalPolicy = (task) => {
178
+ const actionType = task.actionType;
179
+ const pathValue = taskPathFromPayload(task);
180
+ const assignee = task.assigneeAgentId ? store.getAgentById(task.assigneeAgentId) : null;
181
+ const assigneeTitle = assignee?.title.toLowerCase() ?? "";
182
+ if (actionType === "none") {
183
+ return { approvalStatus: "not_required", reason: "Non-file task auto-executes." };
184
+ }
185
+ if (actionType === "file_delete" || actionType === "file_rename") {
186
+ return { approvalStatus: "pending", reason: "High-risk file action always requires approval." };
187
+ }
188
+ const backendTrusted = assigneeTitle.includes("backend") &&
189
+ policyPrefixMatch(pathValue, ["trusted/backend/", "automation/backend/"]);
190
+ const uiTrusted = (assigneeTitle.includes("ui") || assigneeTitle.includes("design")) &&
191
+ policyPrefixMatch(pathValue, ["trusted/ui/", "automation/ui/"]);
192
+ if (backendTrusted || uiTrusted) {
193
+ return {
194
+ approvalStatus: "approved",
195
+ reason: "Trusted agent and trusted path matched auto-approval policy."
196
+ };
197
+ }
198
+ return { approvalStatus: "pending", reason: "Task requires manual approval by default policy." };
199
+ };
200
+ const executeTaskAction = async (task) => {
201
+ const payload = task.actionPayload ?? {};
202
+ switch (task.actionType) {
203
+ case "none":
204
+ return "No file action requested.";
205
+ case "file_write": {
206
+ const filePath = payload.path?.trim();
207
+ if (!filePath) {
208
+ throw new Error("Task payload missing path for file_write");
209
+ }
210
+ const content = payload.content ?? "";
211
+ try {
212
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, filePath);
213
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
214
+ await fs.writeFile(absolutePath, content, "utf8");
215
+ store.recordFileAction({
216
+ actorId: taskActorId(task),
217
+ action: "write",
218
+ targetPath: filePath,
219
+ status: "ok",
220
+ errorMessage: null
221
+ });
222
+ return `Wrote /${filePath}`;
223
+ }
224
+ catch (error) {
225
+ const message = error instanceof Error ? error.message : "Unknown write error";
226
+ store.recordFileAction({
227
+ actorId: taskActorId(task),
228
+ action: "write",
229
+ targetPath: filePath,
230
+ status: "error",
231
+ errorMessage: message
232
+ });
233
+ throw error;
234
+ }
235
+ }
236
+ case "file_mkdir": {
237
+ const directoryPath = payload.path?.trim();
238
+ if (!directoryPath) {
239
+ throw new Error("Task payload missing path for file_mkdir");
240
+ }
241
+ try {
242
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, directoryPath);
243
+ await fs.mkdir(absolutePath, { recursive: true });
244
+ store.recordFileAction({
245
+ actorId: taskActorId(task),
246
+ action: "mkdir",
247
+ targetPath: directoryPath,
248
+ status: "ok",
249
+ errorMessage: null
250
+ });
251
+ return `Created folder /${directoryPath}`;
252
+ }
253
+ catch (error) {
254
+ const message = error instanceof Error ? error.message : "Unknown mkdir error";
255
+ store.recordFileAction({
256
+ actorId: taskActorId(task),
257
+ action: "mkdir",
258
+ targetPath: directoryPath,
259
+ status: "error",
260
+ errorMessage: message
261
+ });
262
+ throw error;
263
+ }
264
+ }
265
+ case "file_rename": {
266
+ const currentPath = payload.path?.trim();
267
+ const nextPath = payload.nextPath?.trim();
268
+ if (!currentPath || !nextPath) {
269
+ throw new Error("Task payload missing path/nextPath for file_rename");
270
+ }
271
+ if (currentPath === "." || nextPath === ".") {
272
+ throw new Error("Cannot rename workspace root");
273
+ }
274
+ try {
275
+ const fromPath = resolveSandboxPath(resolvedConfig.workspaceRoot, currentPath);
276
+ const toPath = resolveSandboxPath(resolvedConfig.workspaceRoot, nextPath);
277
+ try {
278
+ await fs.access(toPath);
279
+ throw new Error(`Destination already exists: ${nextPath}`);
280
+ }
281
+ catch (accessError) {
282
+ const code = accessError.code;
283
+ if (code !== "ENOENT") {
284
+ throw accessError;
285
+ }
286
+ }
287
+ await fs.mkdir(path.dirname(toPath), { recursive: true });
288
+ await fs.rename(fromPath, toPath);
289
+ store.recordFileAction({
290
+ actorId: taskActorId(task),
291
+ action: "rename",
292
+ targetPath: `${currentPath} -> ${nextPath}`,
293
+ status: "ok",
294
+ errorMessage: null
295
+ });
296
+ return `Renamed /${currentPath} to /${nextPath}`;
297
+ }
298
+ catch (error) {
299
+ const message = error instanceof Error ? error.message : "Unknown rename error";
300
+ store.recordFileAction({
301
+ actorId: taskActorId(task),
302
+ action: "rename",
303
+ targetPath: `${currentPath} -> ${nextPath}`,
304
+ status: "error",
305
+ errorMessage: message
306
+ });
307
+ throw error;
308
+ }
309
+ }
310
+ case "file_delete": {
311
+ const targetPath = payload.path?.trim();
312
+ if (!targetPath) {
313
+ throw new Error("Task payload missing path for file_delete");
314
+ }
315
+ if (targetPath === ".") {
316
+ throw new Error("Cannot delete workspace root");
317
+ }
318
+ try {
319
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, targetPath);
320
+ const stats = await fs.lstat(absolutePath);
321
+ if (stats.isDirectory()) {
322
+ await fs.rm(absolutePath, { recursive: true, force: false });
323
+ }
324
+ else {
325
+ await fs.unlink(absolutePath);
326
+ }
327
+ store.recordFileAction({
328
+ actorId: taskActorId(task),
329
+ action: "delete",
330
+ targetPath,
331
+ status: "ok",
332
+ errorMessage: null
333
+ });
334
+ return `Deleted /${targetPath}`;
335
+ }
336
+ catch (error) {
337
+ const message = error instanceof Error ? error.message : "Unknown delete error";
338
+ store.recordFileAction({
339
+ actorId: taskActorId(task),
340
+ action: "delete",
341
+ targetPath,
342
+ status: "error",
343
+ errorMessage: message
344
+ });
345
+ throw error;
346
+ }
347
+ }
348
+ default:
349
+ throw new Error(`Unsupported task action type: ${task.actionType}`);
350
+ }
351
+ };
352
+ const executeTask = async (task, approvalStatusOverride, actorId = "task-system") => {
353
+ const approvalStatus = approvalStatusOverride ?? task.approvalStatus;
354
+ const inProgress = store.updateTaskState({
355
+ taskId: task.id,
356
+ status: "in_progress",
357
+ approvalStatus,
358
+ errorMessage: null,
359
+ completedAt: null
360
+ });
361
+ emitTaskUpdated(inProgress);
362
+ recordAndEmitTaskEvent({
363
+ taskId: inProgress.id,
364
+ projectId: inProgress.projectId,
365
+ eventType: "execution_started",
366
+ actorId,
367
+ detail: `Execution started (${approvalStatus.replace(/_/g, " ")}).`
368
+ });
369
+ try {
370
+ const resultSummary = await executeTaskAction(inProgress);
371
+ const completed = store.updateTaskState({
372
+ taskId: task.id,
373
+ status: "done",
374
+ approvalStatus,
375
+ errorMessage: null
376
+ });
377
+ emitTaskUpdated(completed);
378
+ recordAndEmitTaskEvent({
379
+ taskId: completed.id,
380
+ projectId: completed.projectId,
381
+ eventType: "execution_succeeded",
382
+ actorId,
383
+ detail: resultSummary
384
+ });
385
+ emitTaskMessage(completed, `Task completed: ${completed.title}. ${resultSummary}`, `Approval: ${approvalStatus.replace(/_/g, " ")}\nAction: ${completed.actionType}\nResult: ${resultSummary}`);
386
+ return { task: completed, error: null };
387
+ }
388
+ catch (error) {
389
+ const message = error instanceof Error ? error.message : String(error);
390
+ const blocked = store.updateTaskState({
391
+ taskId: task.id,
392
+ status: "blocked",
393
+ approvalStatus,
394
+ errorMessage: message,
395
+ completedAt: null
396
+ });
397
+ emitTaskUpdated(blocked);
398
+ recordAndEmitTaskEvent({
399
+ taskId: blocked.id,
400
+ projectId: blocked.projectId,
401
+ eventType: "execution_failed",
402
+ actorId,
403
+ detail: message
404
+ });
405
+ emitTaskMessage(blocked, `Task failed: ${blocked.title}. ${message}`, `Approval: ${approvalStatus.replace(/_/g, " ")}\nAction: ${blocked.actionType}\nFailure: ${message}`);
406
+ return { task: blocked, error: message };
407
+ }
408
+ };
409
+ const encryptionSecret = encryptionSecretFromToken(resolvedConfig.auth.encryptionSecret ?? resolvedConfig.auth.token);
410
+ const modelClient = new OpenAICompatibleModelClient();
411
+ const buildBootstrapPayload = (preferredProjectId) => {
412
+ const projects = store.listProjectThreads();
413
+ if (projects.length === 0) {
414
+ throw new Error("No projects available");
415
+ }
416
+ const selectedThread = preferredProjectId
417
+ ? projects.find((projectThread) => projectThread.id === preferredProjectId) ?? projects[0]
418
+ : projects[0];
419
+ const activeProject = store.getProjectById(selectedThread.id);
420
+ if (!activeProject) {
421
+ throw new Error(`Project ${selectedThread.id} not found`);
422
+ }
423
+ return BootstrapPayloadSchema.parse({
424
+ project: activeProject,
425
+ projects,
426
+ projectAgentIds: store.listProjectAgentIds(activeProject.id),
427
+ projectAgentIdsByProject: store.listProjectAgentIdsByProject(),
428
+ agents: store.listAgents(),
429
+ providers: store.listProviderRecords().map(mapProviderRecordToPublic),
430
+ messages: store.listMessages(activeProject.id),
431
+ receiptsByMessage: store.listReceiptsByProject(activeProject.id),
432
+ tasks: store.listTasksByProject(activeProject.id),
433
+ taskEventsByTask: store.listTaskEventsByProject(activeProject.id)
434
+ });
435
+ };
436
+ const runtime = new AgentRuntime(store, emit, async ({ agent, latestMessages }) => {
437
+ if (!agent.providerKey) {
438
+ return {
439
+ body: "I need a configured provider before I can generate real model responses. Set one in Integrations.",
440
+ trace: "Provider missing\nAction: open Settings > Integrations and bind a provider to this agent.",
441
+ model: agent.model
442
+ };
443
+ }
444
+ const providerRecord = store.getProviderRecord(agent.providerKey);
445
+ if (!providerRecord) {
446
+ return {
447
+ body: `My assigned provider '${agent.providerKey}' is missing. Please reconfigure agent routing.`,
448
+ trace: `Provider lookup failed for key: ${agent.providerKey}`,
449
+ model: agent.model
450
+ };
451
+ }
452
+ const provider = toProviderRuntimeConfig(providerRecord, encryptionSecret);
453
+ const generated = await modelClient.generateAgentReply({
454
+ provider,
455
+ model: agent.model,
456
+ agentName: agent.name,
457
+ agentTitle: agent.title,
458
+ agentInstruction: agent.systemPrompt,
459
+ latestMessages,
460
+ includeTrace: agent.traceEnabled
461
+ });
462
+ return {
463
+ body: generated.body,
464
+ trace: generated.trace,
465
+ model: agent.model
466
+ };
467
+ }, async (delegatedTask) => {
468
+ const latestTask = store.getTaskById(delegatedTask.id);
469
+ if (!latestTask) {
470
+ return;
471
+ }
472
+ const policy = resolveApprovalPolicy(latestTask);
473
+ let policyTask = latestTask;
474
+ if (policy.approvalStatus !== latestTask.approvalStatus) {
475
+ policyTask = store.updateTaskState({
476
+ taskId: latestTask.id,
477
+ approvalStatus: policy.approvalStatus,
478
+ errorMessage: null
479
+ });
480
+ emitTaskUpdated(policyTask);
481
+ }
482
+ recordAndEmitTaskEvent({
483
+ taskId: policyTask.id,
484
+ projectId: policyTask.projectId,
485
+ eventType: "policy_applied",
486
+ actorId: "policy-engine",
487
+ detail: policy.reason
488
+ });
489
+ if (policyTask.approvalStatus === "pending") {
490
+ return;
491
+ }
492
+ await executeTask(policyTask, policyTask.approvalStatus, "policy-engine");
493
+ });
494
+ const persistAuthConfig = async () => {
495
+ const raw = await fs.readFile(options.configPath, "utf8");
496
+ const parsed = JSON.parse(raw);
497
+ parsed.auth = {
498
+ ...parsed.auth,
499
+ token: sessionToken,
500
+ passcodeHash: sessionPasscodeHash ?? undefined
501
+ };
502
+ await fs.writeFile(options.configPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
503
+ };
504
+ app.addHook("onRequest", async (request, reply) => {
505
+ if (!request.url.startsWith("/api")) {
506
+ return;
507
+ }
508
+ const token = request.headers["x-cospace-token"];
509
+ if (typeof token !== "string" || token !== sessionToken) {
510
+ reply.code(401).send({ error: "Unauthorized" });
511
+ return;
512
+ }
513
+ const allowWithoutPasscode = request.url.startsWith("/api/session/challenge");
514
+ if (sessionPasscodeHash && !allowWithoutPasscode) {
515
+ const provided = request.headers["x-cospace-passcode"];
516
+ if (typeof provided !== "string" || passcodeHashFromRaw(provided) !== sessionPasscodeHash) {
517
+ reply.code(401).send({ error: "Passcode required" });
518
+ return;
519
+ }
520
+ }
521
+ });
522
+ app.get("/api/session/challenge", async () => {
523
+ return {
524
+ passcodeRequired: Boolean(sessionPasscodeHash),
525
+ warning: sessionPasscodeHash
526
+ ? "Passcode required for API and WebSocket access."
527
+ : "Passcode not configured. Recommended when using public tunnel."
528
+ };
529
+ });
530
+ app.post("/api/session/passcode", async (request, reply) => {
531
+ const body = request.body;
532
+ const provided = typeof body?.passcode === "string" ? body.passcode.trim() : "";
533
+ if (provided.length > 0 && provided.length < 6) {
534
+ reply.code(400);
535
+ return { error: "Passcode must be at least 6 characters" };
536
+ }
537
+ sessionPasscodeHash = provided.length > 0 ? passcodeHashFromRaw(provided) : null;
538
+ await persistAuthConfig();
539
+ for (const socket of sockets) {
540
+ socket.close();
541
+ }
542
+ return {
543
+ ok: true,
544
+ passcodeRequired: Boolean(sessionPasscodeHash)
545
+ };
546
+ });
547
+ app.post("/api/session/rotate-token", async () => {
548
+ sessionToken = crypto.randomBytes(18).toString("hex");
549
+ await persistAuthConfig();
550
+ for (const socket of sockets) {
551
+ socket.close();
552
+ }
553
+ const host = options.host ?? "127.0.0.1";
554
+ const base = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${resolvedConfig.port}`;
555
+ return {
556
+ ok: true,
557
+ token: sessionToken,
558
+ localUrl: `${base}?token=${encodeURIComponent(sessionToken)}`
559
+ };
560
+ });
561
+ app.get("/api/bootstrap", async (request, reply) => {
562
+ const query = request.query;
563
+ if (query.projectId && !store.getProjectById(query.projectId)) {
564
+ reply.code(404);
565
+ return { error: `Project ${query.projectId} not found` };
566
+ }
567
+ return buildBootstrapPayload(query.projectId);
568
+ });
569
+ app.get("/api/projects", async () => {
570
+ return { projects: store.listProjectThreads() };
571
+ });
572
+ app.post("/api/projects", async (request, reply) => {
573
+ const parsed = ProjectCreateInputSchema.parse(request.body);
574
+ const project = store.createProject({ name: parsed.name });
575
+ reply.code(201);
576
+ return {
577
+ project,
578
+ projects: store.listProjectThreads()
579
+ };
580
+ });
581
+ app.put("/api/projects/:projectId/agents", async (request, reply) => {
582
+ const params = request.params;
583
+ const parsed = ProjectAgentAssignmentInputSchema.parse(request.body);
584
+ const project = store.getProjectById(params.projectId);
585
+ if (!project) {
586
+ reply.code(404);
587
+ return { error: `Project ${params.projectId} not found` };
588
+ }
589
+ const knownAgentIds = new Set(store.listAgents().map((agent) => agent.id));
590
+ const unknownAgentId = parsed.agentIds.find((agentId) => !knownAgentIds.has(agentId));
591
+ if (unknownAgentId) {
592
+ reply.code(400);
593
+ return { error: `Unknown agent id: ${unknownAgentId}` };
594
+ }
595
+ const projectAgentIds = store.setProjectAgentIds(project.id, parsed.agentIds);
596
+ return {
597
+ projectAgentIds,
598
+ projects: store.listProjectThreads(),
599
+ projectAgentIdsByProject: store.listProjectAgentIdsByProject()
600
+ };
601
+ });
602
+ app.get("/api/projects/:projectId/tasks", async (request, reply) => {
603
+ const params = request.params;
604
+ const project = store.getProjectById(params.projectId);
605
+ if (!project) {
606
+ reply.code(404);
607
+ return { error: `Project ${params.projectId} not found` };
608
+ }
609
+ return { tasks: store.listTasksByProject(project.id) };
610
+ });
611
+ app.get("/api/projects/:projectId/task-events", async (request, reply) => {
612
+ const params = request.params;
613
+ const project = store.getProjectById(params.projectId);
614
+ if (!project) {
615
+ reply.code(404);
616
+ return { error: `Project ${params.projectId} not found` };
617
+ }
618
+ return { taskEventsByTask: store.listTaskEventsByProject(project.id) };
619
+ });
620
+ app.post("/api/projects/:projectId/tasks", async (request, reply) => {
621
+ const params = request.params;
622
+ const parsed = TaskCreateInputSchema.parse(request.body);
623
+ const project = store.getProjectById(params.projectId);
624
+ if (!project) {
625
+ reply.code(404);
626
+ return { error: `Project ${params.projectId} not found` };
627
+ }
628
+ const assigneeAgentId = parsed.assigneeAgentId?.trim() ?? null;
629
+ if (assigneeAgentId) {
630
+ const assigneeAgent = store.getAgentById(assigneeAgentId);
631
+ if (!assigneeAgent) {
632
+ reply.code(400);
633
+ return { error: `Unknown assignee agent ${assigneeAgentId}` };
634
+ }
635
+ }
636
+ const requestedApprovalStatus = parsed.approvalStatus ?? (parsed.actionType === "none" ? "not_required" : "pending");
637
+ let task = store.createTask({
638
+ projectId: project.id,
639
+ title: parsed.title,
640
+ description: parsed.description?.trim() || null,
641
+ status: "queued",
642
+ approvalStatus: requestedApprovalStatus,
643
+ actionType: parsed.actionType,
644
+ actionPayload: parsed.actionPayload ?? null,
645
+ assigneeAgentId,
646
+ delegatedByMessageId: null,
647
+ createdBy: "web-user"
648
+ });
649
+ emitTaskUpdated(task);
650
+ recordAndEmitTaskEvent({
651
+ taskId: task.id,
652
+ projectId: task.projectId,
653
+ eventType: "created",
654
+ actorId: "web-user",
655
+ detail: "Task created from web panel."
656
+ });
657
+ const policy = resolveApprovalPolicy(task);
658
+ if (policy.approvalStatus !== task.approvalStatus) {
659
+ task = store.updateTaskState({
660
+ taskId: task.id,
661
+ approvalStatus: policy.approvalStatus,
662
+ errorMessage: null
663
+ });
664
+ emitTaskUpdated(task);
665
+ }
666
+ recordAndEmitTaskEvent({
667
+ taskId: task.id,
668
+ projectId: task.projectId,
669
+ eventType: "policy_applied",
670
+ actorId: "policy-engine",
671
+ detail: policy.reason
672
+ });
673
+ if (task.approvalStatus !== "pending") {
674
+ const execution = await executeTask(task, task.approvalStatus, "web-user");
675
+ task = execution.task;
676
+ }
677
+ reply.code(201);
678
+ return { task };
679
+ });
680
+ app.post("/api/tasks/:taskId/approve", async (request, reply) => {
681
+ const params = request.params;
682
+ const task = store.getTaskById(params.taskId);
683
+ if (!task) {
684
+ reply.code(404);
685
+ return { error: `Task ${params.taskId} not found` };
686
+ }
687
+ if (task.approvalStatus !== "pending") {
688
+ reply.code(400);
689
+ return { error: `Task ${params.taskId} is not awaiting approval` };
690
+ }
691
+ recordAndEmitTaskEvent({
692
+ taskId: task.id,
693
+ projectId: task.projectId,
694
+ eventType: "approved",
695
+ actorId: "web-user",
696
+ detail: "Task approved by user."
697
+ });
698
+ const result = await executeTask(task, "approved", "web-user");
699
+ if (result.error) {
700
+ reply.code(500);
701
+ return { error: result.error, task: result.task };
702
+ }
703
+ return { task: result.task };
704
+ });
705
+ app.post("/api/tasks/:taskId/retry", async (request, reply) => {
706
+ const params = request.params;
707
+ const task = store.getTaskById(params.taskId);
708
+ if (!task) {
709
+ reply.code(404);
710
+ return { error: `Task ${params.taskId} not found` };
711
+ }
712
+ if (task.status === "in_progress") {
713
+ reply.code(409);
714
+ return { error: `Task ${params.taskId} is currently running` };
715
+ }
716
+ if (task.status === "done") {
717
+ reply.code(400);
718
+ return { error: `Task ${params.taskId} is already completed` };
719
+ }
720
+ if (task.status !== "blocked" && task.status !== "cancelled") {
721
+ reply.code(400);
722
+ return { error: `Task ${params.taskId} cannot be retried from status ${task.status}` };
723
+ }
724
+ const nextApprovalStatus = task.actionType === "none"
725
+ ? "not_required"
726
+ : task.approvalStatus === "rejected"
727
+ ? "pending"
728
+ : task.approvalStatus;
729
+ const retried = store.updateTaskState({
730
+ taskId: task.id,
731
+ status: "queued",
732
+ approvalStatus: nextApprovalStatus,
733
+ errorMessage: null,
734
+ completedAt: null
735
+ });
736
+ emitTaskUpdated(retried);
737
+ recordAndEmitTaskEvent({
738
+ taskId: retried.id,
739
+ projectId: retried.projectId,
740
+ eventType: "retry_requested",
741
+ actorId: "web-user",
742
+ detail: "Retry requested by user."
743
+ });
744
+ if (retried.approvalStatus === "pending") {
745
+ emitTaskMessage(retried, `Task retried: ${retried.title}. Awaiting approval in the Tasks panel.`, `Approval: pending\nAction: ${retried.actionType}\nReason: retry requested`);
746
+ return { task: retried };
747
+ }
748
+ const result = await executeTask(retried, nextApprovalStatus, "web-user");
749
+ if (result.error) {
750
+ reply.code(500);
751
+ return { error: result.error, task: result.task };
752
+ }
753
+ return { task: result.task };
754
+ });
755
+ app.post("/api/tasks/:taskId/cancel", async (request, reply) => {
756
+ const params = request.params;
757
+ const task = store.getTaskById(params.taskId);
758
+ if (!task) {
759
+ reply.code(404);
760
+ return { error: `Task ${params.taskId} not found` };
761
+ }
762
+ if (task.status === "done" || task.status === "cancelled") {
763
+ reply.code(400);
764
+ return { error: `Task ${params.taskId} cannot be cancelled from status ${task.status}` };
765
+ }
766
+ if (task.status === "in_progress") {
767
+ reply.code(409);
768
+ return { error: `Task ${params.taskId} is currently running` };
769
+ }
770
+ const cancelled = store.updateTaskState({
771
+ taskId: task.id,
772
+ status: "cancelled",
773
+ approvalStatus: task.approvalStatus === "pending" ? "rejected" : task.approvalStatus,
774
+ errorMessage: "Cancelled by user"
775
+ });
776
+ emitTaskUpdated(cancelled);
777
+ recordAndEmitTaskEvent({
778
+ taskId: cancelled.id,
779
+ projectId: cancelled.projectId,
780
+ eventType: "cancelled",
781
+ actorId: "web-user",
782
+ detail: "Task cancelled by user."
783
+ });
784
+ emitTaskMessage(cancelled, `Task cancelled: ${cancelled.title}.`, `Approval: ${cancelled.approvalStatus.replace(/_/g, " ")}\nAction: ${cancelled.actionType}\nReason: cancelled by user`);
785
+ return { task: cancelled };
786
+ });
787
+ app.post("/api/tasks/:taskId/reject", async (request, reply) => {
788
+ const params = request.params;
789
+ const task = store.getTaskById(params.taskId);
790
+ if (!task) {
791
+ reply.code(404);
792
+ return { error: `Task ${params.taskId} not found` };
793
+ }
794
+ if (task.approvalStatus !== "pending") {
795
+ reply.code(400);
796
+ return { error: `Task ${params.taskId} is not awaiting approval` };
797
+ }
798
+ const blocked = store.updateTaskState({
799
+ taskId: task.id,
800
+ status: "blocked",
801
+ approvalStatus: "rejected",
802
+ errorMessage: "Rejected by user",
803
+ completedAt: null
804
+ });
805
+ emitTaskUpdated(blocked);
806
+ recordAndEmitTaskEvent({
807
+ taskId: blocked.id,
808
+ projectId: blocked.projectId,
809
+ eventType: "rejected",
810
+ actorId: "web-user",
811
+ detail: "Task rejected by user."
812
+ });
813
+ emitTaskMessage(blocked, `Task rejected: ${blocked.title}. Waiting for an updated instruction.`, `Approval: rejected\nAction: ${blocked.actionType}\nReason: user rejected task`);
814
+ return { task: blocked };
815
+ });
816
+ app.post("/api/tasks/:taskId/status", async (request, reply) => {
817
+ const params = request.params;
818
+ const parsed = TaskStatusUpdateInputSchema.parse(request.body);
819
+ const task = store.getTaskById(params.taskId);
820
+ if (!task) {
821
+ reply.code(404);
822
+ return { error: `Task ${params.taskId} not found` };
823
+ }
824
+ const updated = store.updateTaskState({
825
+ taskId: task.id,
826
+ status: parsed.status,
827
+ completedAt: parsed.status === "done" || parsed.status === "cancelled" ? undefined : null
828
+ });
829
+ emitTaskUpdated(updated);
830
+ recordAndEmitTaskEvent({
831
+ taskId: updated.id,
832
+ projectId: updated.projectId,
833
+ eventType: "status_changed",
834
+ actorId: "web-user",
835
+ detail: `Status changed to ${updated.status}.`
836
+ });
837
+ return { task: updated };
838
+ });
839
+ app.get("/api/providers", async () => {
840
+ return { providers: store.listProviderRecords().map(mapProviderRecordToPublic) };
841
+ });
842
+ app.put("/api/providers/:providerKey", async (request, reply) => {
843
+ const params = request.params;
844
+ const parsed = ProviderUpsertInputSchema.parse(request.body);
845
+ let providerKey;
846
+ try {
847
+ providerKey = sanitizeProviderKey(params.providerKey);
848
+ }
849
+ catch (error) {
850
+ reply.code(400);
851
+ return { error: error instanceof Error ? error.message : String(error) };
852
+ }
853
+ const encryptedApiKey = parsed.clearApiKey
854
+ ? null
855
+ : parsed.apiKey
856
+ ? encryptProviderSecret(parsed.apiKey, encryptionSecret)
857
+ : undefined;
858
+ const updated = store.upsertProviderRecord({
859
+ providerKey,
860
+ label: parsed.label,
861
+ kind: parsed.kind,
862
+ baseUrl: parsed.baseUrl,
863
+ defaultModel: parsed.defaultModel,
864
+ encryptedApiKey
865
+ });
866
+ return { provider: mapProviderRecordToPublic(updated) };
867
+ });
868
+ app.post("/api/providers/test", async (request, reply) => {
869
+ const parsed = ProviderConnectionTestInputSchema.parse(request.body);
870
+ let provider;
871
+ if (parsed.providerKey) {
872
+ const providerKey = sanitizeProviderKey(parsed.providerKey);
873
+ const record = store.getProviderRecord(providerKey);
874
+ if (!record) {
875
+ reply.code(404);
876
+ return { ok: false, error: `Provider ${providerKey} not found` };
877
+ }
878
+ try {
879
+ provider = toProviderRuntimeConfig(record, encryptionSecret, parsed.apiKey, parsed.baseUrl, parsed.kind);
880
+ }
881
+ catch (error) {
882
+ reply.code(400);
883
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
884
+ }
885
+ }
886
+ else {
887
+ provider = {
888
+ providerKey: "adhoc",
889
+ label: "Ad hoc",
890
+ kind: parsed.kind,
891
+ baseUrl: parsed.baseUrl,
892
+ apiKey: parsed.apiKey
893
+ };
894
+ }
895
+ const startedAt = Date.now();
896
+ try {
897
+ const result = await modelClient.testConnection({
898
+ provider,
899
+ model: parsed.model
900
+ });
901
+ return {
902
+ ok: true,
903
+ latencyMs: Date.now() - startedAt,
904
+ preview: result.preview
905
+ };
906
+ }
907
+ catch (error) {
908
+ reply.code(502);
909
+ return {
910
+ ok: false,
911
+ error: error instanceof Error ? error.message : String(error)
912
+ };
913
+ }
914
+ });
915
+ app.get("/api/agents", async () => {
916
+ return { agents: store.listAgents() };
917
+ });
918
+ app.post("/api/agents", async (request, reply) => {
919
+ const parsed = AgentCreateInputSchema.parse(request.body);
920
+ const providerKey = parsed.providerKey ?? null;
921
+ if (providerKey) {
922
+ const provider = store.getProviderRecord(providerKey);
923
+ if (!provider) {
924
+ reply.code(400);
925
+ return { error: `Provider ${providerKey} is not configured` };
926
+ }
927
+ }
928
+ const projectId = parsed.projectId?.trim();
929
+ if (projectId) {
930
+ const project = store.getProjectById(projectId);
931
+ if (!project) {
932
+ reply.code(404);
933
+ return { error: `Project ${projectId} not found` };
934
+ }
935
+ }
936
+ const agent = store.createAgent({
937
+ name: parsed.name,
938
+ title: parsed.title,
939
+ model: parsed.model,
940
+ providerKey,
941
+ systemPrompt: parsed.systemPrompt ?? null,
942
+ seenEnabled: parsed.seenEnabled,
943
+ traceEnabled: parsed.traceEnabled,
944
+ projectId
945
+ });
946
+ reply.code(201);
947
+ return {
948
+ agent,
949
+ agents: store.listAgents(),
950
+ projects: store.listProjectThreads(),
951
+ projectAgentIdsByProject: store.listProjectAgentIdsByProject(),
952
+ projectAgentIds: projectId ? store.listProjectAgentIds(projectId) : undefined
953
+ };
954
+ });
955
+ app.put("/api/agents/:agentId/config", async (request, reply) => {
956
+ const params = request.params;
957
+ const parsed = AgentBindingUpdateInputSchema.parse(request.body);
958
+ const existingAgent = store.listAgents().find((agent) => agent.id === params.agentId);
959
+ if (!existingAgent) {
960
+ reply.code(404);
961
+ return { error: `Agent ${params.agentId} not found` };
962
+ }
963
+ const providerKey = parsed.providerKey ?? null;
964
+ const model = parsed.model ?? null;
965
+ const seenEnabled = parsed.seenEnabled ?? existingAgent.seenEnabled;
966
+ const traceEnabled = parsed.traceEnabled ?? existingAgent.traceEnabled;
967
+ if (providerKey) {
968
+ const configuredProvider = store.getProviderRecord(providerKey);
969
+ if (!configuredProvider) {
970
+ reply.code(400);
971
+ return { error: `Provider ${providerKey} is not configured` };
972
+ }
973
+ }
974
+ store.setAgentBinding({
975
+ agentId: params.agentId,
976
+ providerKey,
977
+ model
978
+ });
979
+ store.setAgentVisibility({
980
+ agentId: params.agentId,
981
+ seenEnabled,
982
+ traceEnabled
983
+ });
984
+ const updatedAgent = store.listAgents().find((agent) => agent.id === params.agentId);
985
+ if (!updatedAgent) {
986
+ reply.code(500);
987
+ return { error: "Agent update failed" };
988
+ }
989
+ return { agent: updatedAgent };
990
+ });
991
+ app.post("/api/projects/:projectId/messages", async (request, reply) => {
992
+ const params = request.params;
993
+ const parsedBody = ClientMessageInputSchema.parse(request.body);
994
+ const project = store.getProjectById(params.projectId);
995
+ if (!project) {
996
+ reply.code(404);
997
+ return { error: `Project ${params.projectId} not found` };
998
+ }
999
+ const message = store.createHumanMessage({
1000
+ projectId: params.projectId,
1001
+ body: parsedBody.body,
1002
+ senderName: parsedBody.senderName
1003
+ });
1004
+ const agents = store.listAgentsForProject(params.projectId);
1005
+ for (const agent of agents) {
1006
+ if (!agent.seenEnabled) {
1007
+ continue;
1008
+ }
1009
+ store.createSeenReceipt({
1010
+ messageId: message.id,
1011
+ agentId: agent.id,
1012
+ agentName: agent.name,
1013
+ model: agent.model
1014
+ });
1015
+ }
1016
+ emit({ type: "message.created", data: message });
1017
+ emit({
1018
+ type: "seen.updated",
1019
+ data: {
1020
+ messageId: message.id,
1021
+ receipts: store.listReceiptsForMessage(message.id)
1022
+ }
1023
+ });
1024
+ void runtime.reactToHumanMessage(message);
1025
+ reply.code(201);
1026
+ return { message };
1027
+ });
1028
+ app.post("/api/files/read", async (request) => {
1029
+ const input = FileReadInputSchema.parse(request.body);
1030
+ const actorId = input.actorId ?? "manual";
1031
+ try {
1032
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.path);
1033
+ const content = await fs.readFile(absolutePath, "utf8");
1034
+ store.recordFileAction({
1035
+ actorId,
1036
+ action: "read",
1037
+ targetPath: input.path,
1038
+ status: "ok",
1039
+ errorMessage: null
1040
+ });
1041
+ return { path: input.path, content };
1042
+ }
1043
+ catch (error) {
1044
+ const message = error instanceof Error ? error.message : "Unknown read error";
1045
+ store.recordFileAction({
1046
+ actorId,
1047
+ action: "read",
1048
+ targetPath: input.path,
1049
+ status: "error",
1050
+ errorMessage: message
1051
+ });
1052
+ throw error;
1053
+ }
1054
+ });
1055
+ app.post("/api/files/write", async (request) => {
1056
+ const input = FileWriteInputSchema.parse(request.body);
1057
+ const actorId = input.actorId ?? "manual";
1058
+ try {
1059
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.path);
1060
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
1061
+ await fs.writeFile(absolutePath, input.content, "utf8");
1062
+ store.recordFileAction({
1063
+ actorId,
1064
+ action: "write",
1065
+ targetPath: input.path,
1066
+ status: "ok",
1067
+ errorMessage: null
1068
+ });
1069
+ return { ok: true };
1070
+ }
1071
+ catch (error) {
1072
+ const message = error instanceof Error ? error.message : "Unknown write error";
1073
+ store.recordFileAction({
1074
+ actorId,
1075
+ action: "write",
1076
+ targetPath: input.path,
1077
+ status: "error",
1078
+ errorMessage: message
1079
+ });
1080
+ throw error;
1081
+ }
1082
+ });
1083
+ app.post("/api/files/mkdir", async (request) => {
1084
+ const input = FileMkdirInputSchema.parse(request.body);
1085
+ const actorId = input.actorId ?? "manual";
1086
+ try {
1087
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.path);
1088
+ await fs.mkdir(absolutePath, { recursive: true });
1089
+ store.recordFileAction({
1090
+ actorId,
1091
+ action: "mkdir",
1092
+ targetPath: input.path,
1093
+ status: "ok",
1094
+ errorMessage: null
1095
+ });
1096
+ return { ok: true, path: input.path };
1097
+ }
1098
+ catch (error) {
1099
+ const message = error instanceof Error ? error.message : "Unknown mkdir error";
1100
+ store.recordFileAction({
1101
+ actorId,
1102
+ action: "mkdir",
1103
+ targetPath: input.path,
1104
+ status: "error",
1105
+ errorMessage: message
1106
+ });
1107
+ throw error;
1108
+ }
1109
+ });
1110
+ app.post("/api/files/rename", async (request) => {
1111
+ const input = FileRenameInputSchema.parse(request.body);
1112
+ const actorId = input.actorId ?? "manual";
1113
+ if (input.path === "." || input.nextPath === ".") {
1114
+ throw new Error("Cannot rename the workspace root");
1115
+ }
1116
+ try {
1117
+ const fromPath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.path);
1118
+ const toPath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.nextPath);
1119
+ try {
1120
+ await fs.access(toPath);
1121
+ throw new Error(`Destination already exists: ${input.nextPath}`);
1122
+ }
1123
+ catch (accessError) {
1124
+ const code = accessError.code;
1125
+ if (code !== "ENOENT") {
1126
+ throw accessError;
1127
+ }
1128
+ }
1129
+ await fs.mkdir(path.dirname(toPath), { recursive: true });
1130
+ await fs.rename(fromPath, toPath);
1131
+ store.recordFileAction({
1132
+ actorId,
1133
+ action: "rename",
1134
+ targetPath: `${input.path} -> ${input.nextPath}`,
1135
+ status: "ok",
1136
+ errorMessage: null
1137
+ });
1138
+ return { ok: true, path: input.path, nextPath: input.nextPath };
1139
+ }
1140
+ catch (error) {
1141
+ const message = error instanceof Error ? error.message : "Unknown rename error";
1142
+ store.recordFileAction({
1143
+ actorId,
1144
+ action: "rename",
1145
+ targetPath: `${input.path} -> ${input.nextPath}`,
1146
+ status: "error",
1147
+ errorMessage: message
1148
+ });
1149
+ throw error;
1150
+ }
1151
+ });
1152
+ app.post("/api/files/delete", async (request) => {
1153
+ const input = FileDeleteInputSchema.parse(request.body);
1154
+ const actorId = input.actorId ?? "manual";
1155
+ if (input.path === ".") {
1156
+ throw new Error("Cannot delete the workspace root");
1157
+ }
1158
+ try {
1159
+ const absolutePath = resolveSandboxPath(resolvedConfig.workspaceRoot, input.path);
1160
+ const stats = await fs.lstat(absolutePath);
1161
+ if (stats.isDirectory()) {
1162
+ await fs.rm(absolutePath, { recursive: true, force: false });
1163
+ }
1164
+ else {
1165
+ await fs.unlink(absolutePath);
1166
+ }
1167
+ store.recordFileAction({
1168
+ actorId,
1169
+ action: "delete",
1170
+ targetPath: input.path,
1171
+ status: "ok",
1172
+ errorMessage: null
1173
+ });
1174
+ return { ok: true, path: input.path };
1175
+ }
1176
+ catch (error) {
1177
+ const message = error instanceof Error ? error.message : "Unknown delete error";
1178
+ store.recordFileAction({
1179
+ actorId,
1180
+ action: "delete",
1181
+ targetPath: input.path,
1182
+ status: "error",
1183
+ errorMessage: message
1184
+ });
1185
+ throw error;
1186
+ }
1187
+ });
1188
+ app.post("/api/files/list", async (request) => {
1189
+ const input = FileListInputSchema.parse(request.body);
1190
+ const actorId = input.actorId ?? "manual";
1191
+ try {
1192
+ const files = await listSandboxFiles(resolvedConfig.workspaceRoot, input.path);
1193
+ store.recordFileAction({
1194
+ actorId,
1195
+ action: "list",
1196
+ targetPath: input.path,
1197
+ status: "ok",
1198
+ errorMessage: null
1199
+ });
1200
+ return { files };
1201
+ }
1202
+ catch (error) {
1203
+ const message = error instanceof Error ? error.message : "Unknown list error";
1204
+ store.recordFileAction({
1205
+ actorId,
1206
+ action: "list",
1207
+ targetPath: input.path,
1208
+ status: "error",
1209
+ errorMessage: message
1210
+ });
1211
+ throw error;
1212
+ }
1213
+ });
1214
+ const webDistRoot = maybeWebDist(options.webDistPath);
1215
+ if (webDistRoot) {
1216
+ app.get("/", async (_, reply) => {
1217
+ try {
1218
+ const html = await fs.readFile(path.join(webDistRoot, "index.html"), "utf8");
1219
+ reply.type("text/html").send(html);
1220
+ }
1221
+ catch {
1222
+ reply
1223
+ .type("text/html")
1224
+ .send("<h1>CoSpace is running</h1><p>Web bundle missing. Build apps/web and restart. Example: <code>pnpm --filter @cospacehq/web build</code>.</p>");
1225
+ }
1226
+ });
1227
+ app.get("/*", async (request, reply) => {
1228
+ const requested = request.params["*"];
1229
+ const staticPath = path.resolve(webDistRoot, requested);
1230
+ const inDist = staticPath.startsWith(webDistRoot);
1231
+ if (inDist) {
1232
+ try {
1233
+ const buffer = await fs.readFile(staticPath);
1234
+ const mimeType = mimeTypeForStaticFile(staticPath);
1235
+ if (mimeType) {
1236
+ reply.type(mimeType);
1237
+ }
1238
+ return reply.send(buffer);
1239
+ }
1240
+ catch {
1241
+ if (path.extname(requested)) {
1242
+ reply.code(404);
1243
+ return { error: "Asset not found" };
1244
+ }
1245
+ const html = await fs.readFile(path.join(webDistRoot, "index.html"), "utf8");
1246
+ reply.type("text/html").send(html);
1247
+ return;
1248
+ }
1249
+ }
1250
+ reply.code(404).send({ error: "Not found" });
1251
+ });
1252
+ }
1253
+ app.server.on("upgrade", (request, socket, head) => {
1254
+ const token = readTokenFromRequest(request.url ?? "");
1255
+ const passcode = readPasscodeFromRequest(request.url ?? "");
1256
+ if (token !== sessionToken) {
1257
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
1258
+ socket.destroy();
1259
+ return;
1260
+ }
1261
+ if (sessionPasscodeHash && (!passcode || passcodeHashFromRaw(passcode) !== sessionPasscodeHash)) {
1262
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
1263
+ socket.destroy();
1264
+ return;
1265
+ }
1266
+ wss.handleUpgrade(request, socket, head, (ws) => {
1267
+ wss.emit("connection", ws);
1268
+ });
1269
+ });
1270
+ wss.on("connection", (socket) => {
1271
+ sockets.add(socket);
1272
+ socket.send(JSON.stringify({
1273
+ type: "bootstrap",
1274
+ data: buildBootstrapPayload()
1275
+ }));
1276
+ socket.on("close", () => {
1277
+ sockets.delete(socket);
1278
+ });
1279
+ });
1280
+ const host = options.host ?? "127.0.0.1";
1281
+ await app.listen({ port: resolvedConfig.port, host });
1282
+ const localUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${resolvedConfig.port}`;
1283
+ return {
1284
+ localUrl,
1285
+ resolvedConfig,
1286
+ stop: async () => {
1287
+ for (const socket of sockets) {
1288
+ socket.close();
1289
+ }
1290
+ wss.close();
1291
+ store.close();
1292
+ await app.close();
1293
+ }
1294
+ };
1295
+ }
1296
+ //# sourceMappingURL=server.js.map