@femtomc/mu-server 26.2.70 → 26.2.71

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 (42) hide show
  1. package/dist/api/activities.d.ts +2 -0
  2. package/dist/api/activities.js +160 -0
  3. package/dist/api/config.d.ts +2 -0
  4. package/dist/api/config.js +45 -0
  5. package/dist/api/control_plane.d.ts +2 -0
  6. package/dist/api/control_plane.js +28 -0
  7. package/dist/api/cron.d.ts +2 -0
  8. package/dist/api/cron.js +182 -0
  9. package/dist/api/heartbeats.d.ts +2 -0
  10. package/dist/api/heartbeats.js +211 -0
  11. package/dist/api/identities.d.ts +2 -0
  12. package/dist/api/identities.js +103 -0
  13. package/dist/api/runs.d.ts +2 -0
  14. package/dist/api/runs.js +207 -0
  15. package/dist/cli.js +58 -3
  16. package/dist/config.d.ts +4 -21
  17. package/dist/config.js +24 -75
  18. package/dist/control_plane.d.ts +4 -2
  19. package/dist/control_plane.js +226 -25
  20. package/dist/control_plane_bootstrap_helpers.d.ts +2 -1
  21. package/dist/control_plane_bootstrap_helpers.js +11 -1
  22. package/dist/control_plane_contract.d.ts +57 -0
  23. package/dist/control_plane_contract.js +1 -1
  24. package/dist/control_plane_reload.d.ts +63 -0
  25. package/dist/control_plane_reload.js +525 -0
  26. package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
  27. package/dist/control_plane_run_queue_coordinator.js +327 -0
  28. package/dist/control_plane_telegram_generation.js +0 -1
  29. package/dist/control_plane_wake_delivery.d.ts +50 -0
  30. package/dist/control_plane_wake_delivery.js +123 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +2 -0
  33. package/dist/run_queue.d.ts +95 -0
  34. package/dist/run_queue.js +817 -0
  35. package/dist/run_supervisor.d.ts +20 -0
  36. package/dist/run_supervisor.js +25 -1
  37. package/dist/server.d.ts +5 -10
  38. package/dist/server.js +337 -528
  39. package/dist/server_program_orchestration.js +2 -0
  40. package/dist/server_routing.d.ts +3 -2
  41. package/dist/server_routing.js +28 -900
  42. package/package.json +7 -6
@@ -1,11 +1,14 @@
1
1
  import { extname, join, resolve } from "node:path";
2
- import { getControlPlanePaths, IdentityStore, ROLE_SCOPES, } from "@femtomc/mu-control-plane";
2
+ import { activityRoutes } from "./api/activities.js";
3
+ import { configRoutes } from "./api/config.js";
4
+ import { controlPlaneRoutes } from "./api/control_plane.js";
5
+ import { cronRoutes } from "./api/cron.js";
3
6
  import { eventRoutes } from "./api/events.js";
4
7
  import { forumRoutes } from "./api/forum.js";
8
+ import { heartbeatRoutes } from "./api/heartbeats.js";
9
+ import { identityRoutes } from "./api/identities.js";
5
10
  import { issueRoutes } from "./api/issues.js";
6
- import { applyMuConfigPatch, getMuConfigPath, muConfigPresence, redactMuConfigSecrets, } from "./config.js";
7
- import { cronScheduleInputFromBody, hasCronScheduleInput, parseCronTarget, } from "./cron_request.js";
8
- import { normalizeWakeMode } from "./server_types.js";
11
+ import { runRoutes } from "./api/runs.js";
9
12
  const DEFAULT_MIME_TYPES = {
10
13
  ".html": "text/html; charset=utf-8",
11
14
  ".js": "text/javascript; charset=utf-8",
@@ -36,73 +39,23 @@ export function createServerRequestHandler(deps) {
36
39
  if (path === "/healthz" || path === "/health") {
37
40
  return new Response("ok", { status: 200, headers });
38
41
  }
39
- if (path === "/api/config") {
40
- if (request.method === "GET") {
41
- try {
42
- const config = await deps.loadConfigFromDisk();
43
- return Response.json({
44
- repo_root: deps.context.repoRoot,
45
- config_path: getMuConfigPath(deps.context.repoRoot),
46
- config: redactMuConfigSecrets(config),
47
- presence: muConfigPresence(config),
48
- }, { headers });
49
- }
50
- catch (err) {
51
- return Response.json({ error: `failed to read config: ${deps.describeError(err)}` }, { status: 500, headers });
52
- }
53
- }
54
- if (request.method === "POST") {
55
- let body;
56
- try {
57
- body = (await request.json());
58
- }
59
- catch {
60
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
61
- }
62
- if (!body || !("patch" in body)) {
63
- return Response.json({ error: "missing patch payload" }, { status: 400, headers });
64
- }
65
- try {
66
- const base = await deps.loadConfigFromDisk();
67
- const next = applyMuConfigPatch(base, body.patch);
68
- const configPath = await deps.writeConfig(deps.context.repoRoot, next);
69
- return Response.json({
70
- ok: true,
71
- repo_root: deps.context.repoRoot,
72
- config_path: configPath,
73
- config: redactMuConfigSecrets(next),
74
- presence: muConfigPresence(next),
75
- }, { headers });
76
- }
77
- catch (err) {
78
- return Response.json({ error: `failed to write config: ${deps.describeError(err)}` }, { status: 500, headers });
79
- }
80
- }
81
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
82
- }
83
- if (path === "/api/control-plane/reload") {
42
+ if (path === "/api/server/shutdown") {
84
43
  if (request.method !== "POST") {
85
44
  return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
86
45
  }
87
- let reason = "api_control_plane_reload";
88
- try {
89
- const body = (await request.json());
90
- if (typeof body.reason === "string" && body.reason.trim().length > 0) {
91
- reason = body.reason.trim();
92
- }
93
- }
94
- catch {
95
- // ignore invalid body for reason
46
+ if (!deps.initiateShutdown) {
47
+ return Response.json({ error: "shutdown not supported" }, { status: 501, headers });
96
48
  }
97
- const result = await deps.reloadControlPlane(reason);
98
- return Response.json(result, { status: result.ok ? 200 : 500, headers });
49
+ const shutdown = deps.initiateShutdown;
50
+ // Respond before shutting down so the client receives the response.
51
+ setTimeout(() => { void shutdown(); }, 100);
52
+ return Response.json({ ok: true, message: "shutdown initiated" }, { headers });
99
53
  }
100
- if (path === "/api/control-plane/rollback") {
101
- if (request.method !== "POST") {
102
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
103
- }
104
- const result = await deps.reloadControlPlane("rollback");
105
- return Response.json(result, { status: result.ok ? 200 : 500, headers });
54
+ if (path === "/api/config") {
55
+ return configRoutes(request, url, deps, headers);
56
+ }
57
+ if (path === "/api/control-plane/reload" || path === "/api/control-plane/rollback") {
58
+ return controlPlaneRoutes(request, url, deps, headers);
106
59
  }
107
60
  if (path === "/api/status") {
108
61
  const issues = await deps.context.issueStore.list();
@@ -210,845 +163,20 @@ export function createServerRequestHandler(deps) {
210
163
  return Response.json({ error: `command failed: ${deps.describeError(err)}` }, { status: 500, headers });
211
164
  }
212
165
  }
213
- if (path === "/api/runs") {
214
- if (request.method !== "GET") {
215
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
216
- }
217
- const status = url.searchParams.get("status")?.trim() || undefined;
218
- const limitRaw = url.searchParams.get("limit");
219
- const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
220
- const runs = await deps.controlPlaneProxy.listRuns?.({ status, limit });
221
- return Response.json({ count: runs?.length ?? 0, runs: runs ?? [] }, { headers });
222
- }
223
- if (path === "/api/runs/start") {
224
- if (request.method !== "POST") {
225
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
226
- }
227
- let body;
228
- try {
229
- body = (await request.json());
230
- }
231
- catch {
232
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
233
- }
234
- const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
235
- if (prompt.length === 0) {
236
- return Response.json({ error: "prompt is required" }, { status: 400, headers });
237
- }
238
- const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
239
- ? Math.max(1, Math.trunc(body.max_steps))
240
- : undefined;
241
- try {
242
- const run = await deps.controlPlaneProxy.startRun?.({ prompt, maxSteps });
243
- if (!run) {
244
- return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
245
- }
246
- await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
247
- await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
248
- source: "mu-server.runs",
249
- payload: {
250
- action: "register_failed",
251
- run_job_id: run.job_id,
252
- error: deps.describeError(error),
253
- },
254
- });
255
- });
256
- return Response.json({ ok: true, run }, { status: 201, headers });
257
- }
258
- catch (err) {
259
- return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
260
- }
261
- }
262
- if (path === "/api/runs/resume") {
263
- if (request.method !== "POST") {
264
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
265
- }
266
- let body;
267
- try {
268
- body = (await request.json());
269
- }
270
- catch {
271
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
272
- }
273
- const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
274
- if (rootIssueId.length === 0) {
275
- return Response.json({ error: "root_issue_id is required" }, { status: 400, headers });
276
- }
277
- const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
278
- ? Math.max(1, Math.trunc(body.max_steps))
279
- : undefined;
280
- try {
281
- const run = await deps.controlPlaneProxy.resumeRun?.({ rootIssueId, maxSteps });
282
- if (!run) {
283
- return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
284
- }
285
- await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
286
- await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
287
- source: "mu-server.runs",
288
- payload: {
289
- action: "register_failed",
290
- run_job_id: run.job_id,
291
- error: deps.describeError(error),
292
- },
293
- });
294
- });
295
- return Response.json({ ok: true, run }, { status: 201, headers });
296
- }
297
- catch (err) {
298
- return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
299
- }
300
- }
301
- if (path === "/api/runs/interrupt") {
302
- if (request.method !== "POST") {
303
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
304
- }
305
- let body;
306
- try {
307
- body = (await request.json());
308
- }
309
- catch {
310
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
311
- }
312
- const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
313
- const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
314
- const result = await deps.controlPlaneProxy.interruptRun?.({
315
- rootIssueId,
316
- jobId,
317
- });
318
- if (!result) {
319
- return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
320
- }
321
- if (!result.ok && result.reason === "not_running" && result.run) {
322
- await deps.disableAutoRunHeartbeatProgram({
323
- jobId: result.run.job_id,
324
- status: result.run.status,
325
- reason: "interrupt_not_running",
326
- }).catch(() => {
327
- // best effort cleanup only
328
- });
329
- }
330
- return Response.json(result, { status: result.ok ? 200 : 404, headers });
331
- }
332
- if (path === "/api/runs/heartbeat") {
333
- if (request.method !== "POST") {
334
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
335
- }
336
- let body;
337
- try {
338
- body = (await request.json());
339
- }
340
- catch {
341
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
342
- }
343
- const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
344
- const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
345
- const reason = typeof body.reason === "string" ? body.reason.trim() : null;
346
- const wakeMode = normalizeWakeMode(body.wake_mode);
347
- const result = await deps.controlPlaneProxy.heartbeatRun?.({
348
- rootIssueId,
349
- jobId,
350
- reason,
351
- wakeMode,
352
- });
353
- if (!result) {
354
- return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
355
- }
356
- if (!result.ok && result.reason === "not_running" && result.run) {
357
- await deps.disableAutoRunHeartbeatProgram({
358
- jobId: result.run.job_id,
359
- status: result.run.status,
360
- reason: "run_not_running",
361
- }).catch(() => {
362
- // best effort cleanup only
363
- });
364
- }
365
- if (result.ok) {
366
- return Response.json(result, { status: 200, headers });
367
- }
368
- if (result.reason === "missing_target") {
369
- return Response.json(result, { status: 400, headers });
370
- }
371
- if (result.reason === "not_running") {
372
- return Response.json(result, { status: 409, headers });
373
- }
374
- return Response.json(result, { status: 404, headers });
375
- }
376
- if (path.startsWith("/api/runs/")) {
377
- const rest = path.slice("/api/runs/".length);
378
- const [rawId, maybeSub] = rest.split("/");
379
- const idOrRoot = decodeURIComponent(rawId ?? "").trim();
380
- if (idOrRoot.length === 0) {
381
- return Response.json({ error: "missing run id" }, { status: 400, headers });
382
- }
383
- if (maybeSub === "trace") {
384
- if (request.method !== "GET") {
385
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
386
- }
387
- const limitRaw = url.searchParams.get("limit");
388
- const limit = limitRaw && /^\d+$/.test(limitRaw)
389
- ? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
390
- : undefined;
391
- const trace = await deps.controlPlaneProxy.traceRun?.({ idOrRoot, limit });
392
- if (!trace) {
393
- return Response.json({ error: "run trace not found" }, { status: 404, headers });
394
- }
395
- return Response.json(trace, { headers });
396
- }
397
- if (request.method !== "GET") {
398
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
399
- }
400
- const run = await deps.controlPlaneProxy.getRun?.(idOrRoot);
401
- if (!run) {
402
- return Response.json({ error: "run not found" }, { status: 404, headers });
403
- }
404
- if (run.status !== "running") {
405
- await deps.disableAutoRunHeartbeatProgram({
406
- jobId: run.job_id,
407
- status: run.status,
408
- reason: "run_terminal_snapshot",
409
- }).catch(() => {
410
- // best effort cleanup only
411
- });
412
- }
413
- return Response.json(run, { headers });
414
- }
415
- if (path === "/api/cron/status") {
416
- if (request.method !== "GET") {
417
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
418
- }
419
- const status = await deps.cronPrograms.status();
420
- return Response.json(status, { headers });
421
- }
422
- if (path === "/api/cron") {
423
- if (request.method !== "GET") {
424
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
425
- }
426
- const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
427
- const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
428
- const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
429
- const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
430
- const scheduleKindRaw = url.searchParams.get("schedule_kind")?.trim().toLowerCase();
431
- const scheduleKind = scheduleKindRaw === "at" || scheduleKindRaw === "every" || scheduleKindRaw === "cron"
432
- ? scheduleKindRaw
433
- : undefined;
434
- const limitRaw = url.searchParams.get("limit");
435
- const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
436
- const programs = await deps.cronPrograms.list({ enabled, targetKind, scheduleKind, limit });
437
- return Response.json({ count: programs.length, programs }, { headers });
438
- }
439
- if (path === "/api/cron/create") {
440
- if (request.method !== "POST") {
441
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
442
- }
443
- let body;
444
- try {
445
- body = (await request.json());
446
- }
447
- catch {
448
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
449
- }
450
- const title = typeof body.title === "string" ? body.title.trim() : "";
451
- if (!title) {
452
- return Response.json({ error: "title is required" }, { status: 400, headers });
453
- }
454
- const parsedTarget = parseCronTarget(body);
455
- if (!parsedTarget.target) {
456
- return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
457
- }
458
- if (!hasCronScheduleInput(body)) {
459
- return Response.json({ error: "schedule is required" }, { status: 400, headers });
460
- }
461
- const schedule = cronScheduleInputFromBody(body);
462
- const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
463
- const wakeMode = normalizeWakeMode(body.wake_mode);
464
- const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
465
- try {
466
- const program = await deps.cronPrograms.create({
467
- title,
468
- target: parsedTarget.target,
469
- schedule,
470
- reason,
471
- wakeMode,
472
- enabled,
473
- metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
474
- ? body.metadata
475
- : undefined,
476
- });
477
- return Response.json({ ok: true, program }, { status: 201, headers });
478
- }
479
- catch (err) {
480
- return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
481
- }
482
- }
483
- if (path === "/api/cron/update") {
484
- if (request.method !== "POST") {
485
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
486
- }
487
- let body;
488
- try {
489
- body = (await request.json());
490
- }
491
- catch {
492
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
493
- }
494
- const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
495
- if (!programId) {
496
- return Response.json({ error: "program_id is required" }, { status: 400, headers });
497
- }
498
- let target;
499
- if (typeof body.target_kind === "string") {
500
- const parsedTarget = parseCronTarget(body);
501
- if (!parsedTarget.target) {
502
- return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
503
- }
504
- target = parsedTarget.target;
505
- }
506
- const schedule = hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined;
507
- const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
508
- try {
509
- const result = await deps.cronPrograms.update({
510
- programId,
511
- title: typeof body.title === "string" ? body.title : undefined,
512
- reason: typeof body.reason === "string" ? body.reason : undefined,
513
- wakeMode,
514
- enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
515
- target,
516
- schedule,
517
- metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
518
- ? body.metadata
519
- : undefined,
520
- });
521
- if (result.ok) {
522
- return Response.json(result, { headers });
523
- }
524
- if (result.reason === "not_found") {
525
- return Response.json(result, { status: 404, headers });
526
- }
527
- return Response.json(result, { status: 400, headers });
528
- }
529
- catch (err) {
530
- return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
531
- }
532
- }
533
- if (path === "/api/cron/delete") {
534
- if (request.method !== "POST") {
535
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
536
- }
537
- let body;
538
- try {
539
- body = (await request.json());
540
- }
541
- catch {
542
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
543
- }
544
- const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
545
- if (!programId) {
546
- return Response.json({ error: "program_id is required" }, { status: 400, headers });
547
- }
548
- const result = await deps.cronPrograms.remove(programId);
549
- return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
550
- }
551
- if (path === "/api/cron/trigger") {
552
- if (request.method !== "POST") {
553
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
554
- }
555
- let body;
556
- try {
557
- body = (await request.json());
558
- }
559
- catch {
560
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
561
- }
562
- const result = await deps.cronPrograms.trigger({
563
- programId: typeof body.program_id === "string" ? body.program_id : null,
564
- reason: typeof body.reason === "string" ? body.reason : null,
565
- });
566
- if (result.ok) {
567
- return Response.json(result, { headers });
568
- }
569
- if (result.reason === "missing_target") {
570
- return Response.json(result, { status: 400, headers });
571
- }
572
- if (result.reason === "not_found") {
573
- return Response.json(result, { status: 404, headers });
574
- }
575
- return Response.json(result, { status: 409, headers });
576
- }
577
- if (path.startsWith("/api/cron/")) {
578
- if (request.method !== "GET") {
579
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
580
- }
581
- const id = decodeURIComponent(path.slice("/api/cron/".length)).trim();
582
- if (!id) {
583
- return Response.json({ error: "missing program id" }, { status: 400, headers });
584
- }
585
- const program = await deps.cronPrograms.get(id);
586
- if (!program) {
587
- return Response.json({ error: "program not found" }, { status: 404, headers });
588
- }
589
- return Response.json(program, { headers });
590
- }
591
- if (path === "/api/heartbeats") {
592
- if (request.method !== "GET") {
593
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
594
- }
595
- const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
596
- const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
597
- const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
598
- const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
599
- const limitRaw = url.searchParams.get("limit");
600
- const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
601
- const programs = await deps.heartbeatPrograms.list({ enabled, targetKind, limit });
602
- return Response.json({ count: programs.length, programs }, { headers });
603
- }
604
- if (path === "/api/heartbeats/create") {
605
- if (request.method !== "POST") {
606
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
607
- }
608
- let body;
609
- try {
610
- body = (await request.json());
611
- }
612
- catch {
613
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
614
- }
615
- const title = typeof body.title === "string" ? body.title.trim() : "";
616
- if (!title) {
617
- return Response.json({ error: "title is required" }, { status: 400, headers });
618
- }
619
- const targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
620
- let target = null;
621
- if (targetKind === "run") {
622
- const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
623
- const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
624
- if (!jobId && !rootIssueId) {
625
- return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
626
- }
627
- target = {
628
- kind: "run",
629
- job_id: jobId || null,
630
- root_issue_id: rootIssueId || null,
631
- };
632
- }
633
- else if (targetKind === "activity") {
634
- const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
635
- if (!activityId) {
636
- return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
637
- }
638
- target = {
639
- kind: "activity",
640
- activity_id: activityId,
641
- };
642
- }
643
- else {
644
- return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
645
- }
646
- const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
647
- ? Math.max(0, Math.trunc(body.every_ms))
648
- : undefined;
649
- const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
650
- const wakeMode = normalizeWakeMode(body.wake_mode);
651
- const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
652
- try {
653
- const program = await deps.heartbeatPrograms.create({
654
- title,
655
- target,
656
- everyMs,
657
- reason,
658
- wakeMode,
659
- enabled,
660
- metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
661
- ? body.metadata
662
- : undefined,
663
- });
664
- return Response.json({ ok: true, program }, { status: 201, headers });
665
- }
666
- catch (err) {
667
- return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
668
- }
669
- }
670
- if (path === "/api/heartbeats/update") {
671
- if (request.method !== "POST") {
672
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
673
- }
674
- let body;
675
- try {
676
- body = (await request.json());
677
- }
678
- catch {
679
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
680
- }
681
- const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
682
- if (!programId) {
683
- return Response.json({ error: "program_id is required" }, { status: 400, headers });
684
- }
685
- let target;
686
- if (typeof body.target_kind === "string") {
687
- const targetKind = body.target_kind.trim().toLowerCase();
688
- if (targetKind === "run") {
689
- const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
690
- const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
691
- if (!jobId && !rootIssueId) {
692
- return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
693
- }
694
- target = {
695
- kind: "run",
696
- job_id: jobId || null,
697
- root_issue_id: rootIssueId || null,
698
- };
699
- }
700
- else if (targetKind === "activity") {
701
- const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
702
- if (!activityId) {
703
- return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
704
- }
705
- target = {
706
- kind: "activity",
707
- activity_id: activityId,
708
- };
709
- }
710
- else {
711
- return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
712
- }
713
- }
714
- const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
715
- try {
716
- const result = await deps.heartbeatPrograms.update({
717
- programId,
718
- title: typeof body.title === "string" ? body.title : undefined,
719
- target,
720
- everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
721
- ? Math.max(0, Math.trunc(body.every_ms))
722
- : undefined,
723
- reason: typeof body.reason === "string" ? body.reason : undefined,
724
- wakeMode,
725
- enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
726
- metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
727
- ? body.metadata
728
- : undefined,
729
- });
730
- if (result.ok) {
731
- return Response.json(result, { headers });
732
- }
733
- return Response.json(result, { status: result.reason === "not_found" ? 404 : 400, headers });
734
- }
735
- catch (err) {
736
- return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
737
- }
738
- }
739
- if (path === "/api/heartbeats/delete") {
740
- if (request.method !== "POST") {
741
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
742
- }
743
- let body;
744
- try {
745
- body = (await request.json());
746
- }
747
- catch {
748
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
749
- }
750
- const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
751
- if (!programId) {
752
- return Response.json({ error: "program_id is required" }, { status: 400, headers });
753
- }
754
- const result = await deps.heartbeatPrograms.remove(programId);
755
- return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
756
- }
757
- if (path === "/api/heartbeats/trigger") {
758
- if (request.method !== "POST") {
759
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
760
- }
761
- let body;
762
- try {
763
- body = (await request.json());
764
- }
765
- catch {
766
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
767
- }
768
- const result = await deps.heartbeatPrograms.trigger({
769
- programId: typeof body.program_id === "string" ? body.program_id : null,
770
- reason: typeof body.reason === "string" ? body.reason : null,
771
- });
772
- if (result.ok) {
773
- return Response.json(result, { headers });
774
- }
775
- if (result.reason === "missing_target") {
776
- return Response.json(result, { status: 400, headers });
777
- }
778
- if (result.reason === "not_found") {
779
- return Response.json(result, { status: 404, headers });
780
- }
781
- return Response.json(result, { status: 409, headers });
782
- }
783
- if (path.startsWith("/api/heartbeats/")) {
784
- if (request.method !== "GET") {
785
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
786
- }
787
- const id = decodeURIComponent(path.slice("/api/heartbeats/".length)).trim();
788
- if (!id) {
789
- return Response.json({ error: "missing program id" }, { status: 400, headers });
790
- }
791
- const program = await deps.heartbeatPrograms.get(id);
792
- if (!program) {
793
- return Response.json({ error: "program not found" }, { status: 404, headers });
794
- }
795
- return Response.json(program, { headers });
796
- }
797
- if (path === "/api/activities") {
798
- if (request.method !== "GET") {
799
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
800
- }
801
- const statusRaw = url.searchParams.get("status")?.trim().toLowerCase();
802
- const status = statusRaw === "running" || statusRaw === "completed" || statusRaw === "failed" || statusRaw === "cancelled"
803
- ? statusRaw
804
- : undefined;
805
- const kind = url.searchParams.get("kind")?.trim() || undefined;
806
- const limitRaw = url.searchParams.get("limit");
807
- const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
808
- const activities = deps.activitySupervisor.list({ status, kind, limit });
809
- return Response.json({ count: activities.length, activities }, { headers });
166
+ if (path === "/api/runs" || path.startsWith("/api/runs/")) {
167
+ return runRoutes(request, url, deps, headers);
810
168
  }
811
- if (path === "/api/activities/start") {
812
- if (request.method !== "POST") {
813
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
814
- }
815
- let body;
816
- try {
817
- body = (await request.json());
818
- }
819
- catch {
820
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
821
- }
822
- const title = typeof body.title === "string" ? body.title.trim() : "";
823
- if (!title) {
824
- return Response.json({ error: "title is required" }, { status: 400, headers });
825
- }
826
- const kind = typeof body.kind === "string" ? body.kind.trim() : undefined;
827
- const heartbeatEveryMs = typeof body.heartbeat_every_ms === "number" && Number.isFinite(body.heartbeat_every_ms)
828
- ? Math.max(0, Math.trunc(body.heartbeat_every_ms))
829
- : undefined;
830
- const source = body.source === "api" || body.source === "command" || body.source === "system" ? body.source : "api";
831
- try {
832
- const activity = deps.activitySupervisor.start({
833
- title,
834
- kind,
835
- heartbeatEveryMs,
836
- metadata: body.metadata ?? undefined,
837
- source,
838
- });
839
- return Response.json({ ok: true, activity }, { status: 201, headers });
840
- }
841
- catch (err) {
842
- return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
843
- }
169
+ if (path === "/api/cron" || path.startsWith("/api/cron/")) {
170
+ return cronRoutes(request, url, deps, headers);
844
171
  }
845
- if (path === "/api/activities/progress") {
846
- if (request.method !== "POST") {
847
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
848
- }
849
- let body;
850
- try {
851
- body = (await request.json());
852
- }
853
- catch {
854
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
855
- }
856
- const result = deps.activitySupervisor.progress({
857
- activityId: typeof body.activity_id === "string" ? body.activity_id : null,
858
- message: typeof body.message === "string" ? body.message : null,
859
- });
860
- if (result.ok) {
861
- return Response.json(result, { headers });
862
- }
863
- if (result.reason === "missing_target") {
864
- return Response.json(result, { status: 400, headers });
865
- }
866
- if (result.reason === "not_running") {
867
- return Response.json(result, { status: 409, headers });
868
- }
869
- return Response.json(result, { status: 404, headers });
172
+ if (path === "/api/heartbeats" || path.startsWith("/api/heartbeats/")) {
173
+ return heartbeatRoutes(request, url, deps, headers);
870
174
  }
871
- if (path === "/api/activities/heartbeat") {
872
- if (request.method !== "POST") {
873
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
874
- }
875
- let body;
876
- try {
877
- body = (await request.json());
878
- }
879
- catch {
880
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
881
- }
882
- const result = deps.activitySupervisor.heartbeat({
883
- activityId: typeof body.activity_id === "string" ? body.activity_id : null,
884
- reason: typeof body.reason === "string" ? body.reason : null,
885
- });
886
- if (result.ok) {
887
- return Response.json(result, { headers });
888
- }
889
- if (result.reason === "missing_target") {
890
- return Response.json(result, { status: 400, headers });
891
- }
892
- if (result.reason === "not_running") {
893
- return Response.json(result, { status: 409, headers });
894
- }
895
- return Response.json(result, { status: 404, headers });
896
- }
897
- if (path === "/api/activities/complete" || path === "/api/activities/fail" || path === "/api/activities/cancel") {
898
- if (request.method !== "POST") {
899
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
900
- }
901
- let body;
902
- try {
903
- body = (await request.json());
904
- }
905
- catch {
906
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
907
- }
908
- const activityId = typeof body.activity_id === "string" ? body.activity_id : null;
909
- const message = typeof body.message === "string" ? body.message : null;
910
- const result = path === "/api/activities/complete"
911
- ? deps.activitySupervisor.complete({ activityId, message })
912
- : path === "/api/activities/fail"
913
- ? deps.activitySupervisor.fail({ activityId, message })
914
- : deps.activitySupervisor.cancel({ activityId, message });
915
- if (result.ok) {
916
- return Response.json(result, { headers });
917
- }
918
- if (result.reason === "missing_target") {
919
- return Response.json(result, { status: 400, headers });
920
- }
921
- if (result.reason === "not_running") {
922
- return Response.json(result, { status: 409, headers });
923
- }
924
- return Response.json(result, { status: 404, headers });
925
- }
926
- if (path.startsWith("/api/activities/")) {
927
- if (request.method !== "GET") {
928
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
929
- }
930
- const rest = path.slice("/api/activities/".length);
931
- const [rawId, maybeSub] = rest.split("/");
932
- const activityId = decodeURIComponent(rawId ?? "").trim();
933
- if (activityId.length === 0) {
934
- return Response.json({ error: "missing activity id" }, { status: 400, headers });
935
- }
936
- if (maybeSub === "events") {
937
- const limitRaw = url.searchParams.get("limit");
938
- const limit = limitRaw && /^\d+$/.test(limitRaw)
939
- ? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
940
- : undefined;
941
- const events = deps.activitySupervisor.events(activityId, { limit });
942
- if (!events) {
943
- return Response.json({ error: "activity not found" }, { status: 404, headers });
944
- }
945
- return Response.json({ count: events.length, events }, { headers });
946
- }
947
- const activity = deps.activitySupervisor.get(activityId);
948
- if (!activity) {
949
- return Response.json({ error: "activity not found" }, { status: 404, headers });
950
- }
951
- return Response.json(activity, { headers });
175
+ if (path === "/api/activities" || path.startsWith("/api/activities/")) {
176
+ return activityRoutes(request, url, deps, headers);
952
177
  }
953
178
  if (path === "/api/identities" || path === "/api/identities/link" || path === "/api/identities/unlink") {
954
- const cpPaths = getControlPlanePaths(deps.context.repoRoot);
955
- const identityStore = new IdentityStore(cpPaths.identitiesPath);
956
- await identityStore.load();
957
- if (path === "/api/identities") {
958
- if (request.method !== "GET") {
959
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
960
- }
961
- const includeInactive = url.searchParams.get("include_inactive")?.trim().toLowerCase() === "true";
962
- const bindings = identityStore.listBindings({ includeInactive });
963
- return Response.json({ count: bindings.length, bindings }, { headers });
964
- }
965
- if (path === "/api/identities/link") {
966
- if (request.method !== "POST") {
967
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
968
- }
969
- let body;
970
- try {
971
- body = (await request.json());
972
- }
973
- catch {
974
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
975
- }
976
- const channel = typeof body.channel === "string" ? body.channel.trim() : "";
977
- if (!channel || (channel !== "slack" && channel !== "discord" && channel !== "telegram")) {
978
- return Response.json({ error: "channel is required (slack, discord, telegram)" }, { status: 400, headers });
979
- }
980
- const actorId = typeof body.actor_id === "string" ? body.actor_id.trim() : "";
981
- if (!actorId) {
982
- return Response.json({ error: "actor_id is required" }, { status: 400, headers });
983
- }
984
- const tenantId = typeof body.tenant_id === "string" ? body.tenant_id.trim() : "";
985
- if (!tenantId) {
986
- return Response.json({ error: "tenant_id is required" }, { status: 400, headers });
987
- }
988
- const roleKey = typeof body.role === "string" ? body.role.trim() : "operator";
989
- const roleScopes = ROLE_SCOPES[roleKey];
990
- if (!roleScopes) {
991
- return Response.json({ error: `invalid role: ${roleKey} (operator, contributor, viewer)` }, { status: 400, headers });
992
- }
993
- const bindingId = typeof body.binding_id === "string" && body.binding_id.trim().length > 0
994
- ? body.binding_id.trim()
995
- : `bind-${crypto.randomUUID()}`;
996
- const operatorId = typeof body.operator_id === "string" && body.operator_id.trim().length > 0
997
- ? body.operator_id.trim()
998
- : "default";
999
- const decision = await identityStore.link({
1000
- bindingId,
1001
- operatorId,
1002
- channel: channel,
1003
- channelTenantId: tenantId,
1004
- channelActorId: actorId,
1005
- scopes: [...roleScopes],
1006
- });
1007
- switch (decision.kind) {
1008
- case "linked":
1009
- return Response.json({ ok: true, kind: "linked", binding: decision.binding }, { status: 201, headers });
1010
- case "binding_exists":
1011
- return Response.json({ ok: false, kind: "binding_exists", binding: decision.binding }, { status: 409, headers });
1012
- case "principal_already_linked":
1013
- return Response.json({ ok: false, kind: "principal_already_linked", binding: decision.binding }, { status: 409, headers });
1014
- }
1015
- }
1016
- if (path === "/api/identities/unlink") {
1017
- if (request.method !== "POST") {
1018
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1019
- }
1020
- let body;
1021
- try {
1022
- body = (await request.json());
1023
- }
1024
- catch {
1025
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
1026
- }
1027
- const bindingId = typeof body.binding_id === "string" ? body.binding_id.trim() : "";
1028
- if (!bindingId) {
1029
- return Response.json({ error: "binding_id is required" }, { status: 400, headers });
1030
- }
1031
- const actorBindingId = typeof body.actor_binding_id === "string" ? body.actor_binding_id.trim() : "";
1032
- if (!actorBindingId) {
1033
- return Response.json({ error: "actor_binding_id is required" }, { status: 400, headers });
1034
- }
1035
- const reason = typeof body.reason === "string" ? body.reason.trim() : null;
1036
- const decision = await identityStore.unlinkSelf({
1037
- bindingId,
1038
- actorBindingId,
1039
- reason: reason || null,
1040
- });
1041
- switch (decision.kind) {
1042
- case "unlinked":
1043
- return Response.json({ ok: true, kind: "unlinked", binding: decision.binding }, { headers });
1044
- case "not_found":
1045
- return Response.json({ ok: false, kind: "not_found" }, { status: 404, headers });
1046
- case "invalid_actor":
1047
- return Response.json({ ok: false, kind: "invalid_actor" }, { status: 403, headers });
1048
- case "already_inactive":
1049
- return Response.json({ ok: false, kind: "already_inactive", binding: decision.binding }, { status: 409, headers });
1050
- }
1051
- }
179
+ return identityRoutes(request, url, deps, headers);
1052
180
  }
1053
181
  if (path.startsWith("/api/issues")) {
1054
182
  const response = await issueRoutes(request, deps.context);