@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.5

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 (132) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/infrastructure/harness/atomic-write.js +23 -0
  4. package/dist/worker/console/chat/chat-event-store.js +62 -4
  5. package/dist/worker/console/chat/context-panel.js +7 -0
  6. package/dist/worker/console/chat/deferred-turn.js +12 -0
  7. package/dist/worker/console/chat/pi-runtime.js +264 -16
  8. package/dist/worker/console/chat/provider-error.js +98 -0
  9. package/dist/worker/console/chat/routes.js +292 -29
  10. package/dist/worker/console/chat/session-stats.js +179 -0
  11. package/dist/worker/console/chat/session-store.js +93 -63
  12. package/dist/worker/console/chat/todos.js +115 -0
  13. package/dist/worker/console/chat/tool-preview.js +42 -0
  14. package/dist/worker/console/chat/turn-process.js +20 -55
  15. package/dist/worker/console/chat/user-questions.js +266 -0
  16. package/dist/worker/console/chat/workspace-landing.js +14 -0
  17. package/dist/worker/console/native-directory-picker.js +13 -0
  18. package/dist/worker/console/routes.js +309 -1
  19. package/dist/worker/console/security.js +4 -4
  20. package/dist/worker/console/server.js +529 -186
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
  53. package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
  54. package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +29 -4
  80. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  81. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  82. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  83. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  84. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  85. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  86. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  87. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  88. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  89. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  91. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  92. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  93. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  95. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  96. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  97. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  98. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  99. package/dist/worker/console/workspace-context.js +234 -0
  100. package/dist/worker/console/workspace-registry.js +214 -0
  101. package/dist/worker/observe/server.js +4 -0
  102. package/dist/worker/observe/static/api.js +26 -6
  103. package/dist/worker/observe/static/app.js +18 -70
  104. package/dist/worker/observe/static/index.html +1 -1
  105. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  106. package/dist/worker/observe/static/operator-chrome.css +145 -20
  107. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  108. package/dist/worker/observe/static/operator-chrome.js +330 -100
  109. package/dist/worker/observe/static/router.d.ts +46 -0
  110. package/dist/worker/observe/static/router.js +25 -1
  111. package/dist/worker/observe/static/state.d.ts +50 -0
  112. package/dist/worker/observe/static/state.js +172 -0
  113. package/dist/worker/observe/static/views/dag-inspector.js +3 -2
  114. package/dist/worker/observe/static/views/dag.js +2 -1
  115. package/dist/worker/observe/static/views/dashboard.js +6 -4
  116. package/dist/worker/observe/static/views/night.js +2 -2
  117. package/dist/worker/observe/static/views/pool.js +1 -0
  118. package/dist/worker/observe/static/views/run.js +3 -1
  119. package/dist/worker/observe/static/views/task.js +1 -1
  120. package/dist/workflows/dag/interrupt-request.js +3 -3
  121. package/dist/workflows/dag/lifecycle.js +3 -3
  122. package/docs/README.md +1 -0
  123. package/docs/operations/local-development-environment.md +21 -0
  124. package/harness.json +3 -3
  125. package/package.json +3 -1
  126. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  127. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  128. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  129. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  130. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  131. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -3,10 +3,12 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { dispatchOperatorAction } from "./operator-actions.js";
5
5
  import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
6
+ import { probePiReadiness } from "./pi-readiness.js";
6
7
  import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
7
8
  import { ROUTES } from "../observe/routes.js";
8
9
  import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
9
10
  import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
11
+ import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
10
12
  export function sendJson(res, status, body, extraHeaders) {
11
13
  const payload = JSON.stringify(body);
12
14
  res.writeHead(status, {
@@ -38,6 +40,10 @@ function contentTypeFor(pathname) {
38
40
  return "application/octet-stream";
39
41
  }
40
42
  async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
43
+ // Body pre-read by the multi-workspace dispatcher (workspaceId resolution).
44
+ const preRead = req.__preReadBody;
45
+ if (preRead !== undefined)
46
+ return preRead;
41
47
  const chunks = [];
42
48
  let total = 0;
43
49
  for await (const chunk of req) {
@@ -245,7 +251,35 @@ async function handleGetOperation(_req, res, ctx, operationId) {
245
251
  });
246
252
  return;
247
253
  }
248
- sendJson(res, 200, { ok: true, operation: op });
254
+ sendJson(res, 200, {
255
+ ok: true,
256
+ workspaceId: ctx.workspace?.workspaceId,
257
+ operation: { ...op, workspaceId: ctx.workspace?.workspaceId },
258
+ });
259
+ }
260
+ /**
261
+ * GET /api/operator/v1/operations?workspaceId= — NEW per-workspace list route
262
+ * (multi-workspace foundation). Returns only the resolved Context's Operations
263
+ * and injects the owning workspaceId. operation-store.ts is unchanged.
264
+ */
265
+ async function handleListOperations(_req, res, ctx) {
266
+ if (!ctx.operator) {
267
+ sendJson(res, 503, {
268
+ ok: false,
269
+ error: {
270
+ code: "INTERNAL_ERROR",
271
+ message: "operator runtime unavailable",
272
+ },
273
+ });
274
+ return;
275
+ }
276
+ const operations = await ctx.operator.operations.list();
277
+ sendJson(res, 200, {
278
+ ok: true,
279
+ schemaVersion: 1,
280
+ workspaceId: ctx.workspace?.workspaceId,
281
+ ops: operations.map((op) => ({ ...op, workspaceId: ctx.workspace?.workspaceId })),
282
+ });
249
283
  }
250
284
  async function handleOperationEvents(req, res, ctx, operationId) {
251
285
  if (!ctx.operator) {
@@ -453,6 +487,10 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
453
487
  await handleCreateOperation(req, res, ctx);
454
488
  return true;
455
489
  }
490
+ if (method === "GET" && pathname === "/api/operator/v1/operations") {
491
+ await handleListOperations(req, res, ctx);
492
+ return true;
493
+ }
456
494
  if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
457
495
  await handleNightAuxTickerGet(req, res, ctx);
458
496
  return true;
@@ -517,3 +555,273 @@ export async function handleConsoleRequest(req, res, ctx) {
517
555
  }
518
556
  return false;
519
557
  }
558
+ export function createWorkspaceReadinessCache() {
559
+ const entries = new Map();
560
+ return {
561
+ get: (repoRoot) => entries.get(repoRoot),
562
+ set: (repoRoot, entry) => {
563
+ entries.set(repoRoot, entry);
564
+ },
565
+ };
566
+ }
567
+ const defaultWorkspaceReadinessCache = createWorkspaceReadinessCache();
568
+ const READY_STATE_TTL_MS = 60_000;
569
+ const NON_READY_STATE_TTL_MS = 15_000;
570
+ function isCacheFresh(entry, now) {
571
+ const ttl = entry.state === "ready" ? READY_STATE_TTL_MS : NON_READY_STATE_TTL_MS;
572
+ return now - entry.probedAt < ttl;
573
+ }
574
+ /** Readiness placeholder for a never-probed workspace (AC-006): the list is
575
+ * still returned immediately with a pending marker, refreshed by the
576
+ * background probe. */
577
+ function pendingReadinessSummary(inspectReady) {
578
+ return {
579
+ state: "pending",
580
+ summary: `pi=pending; inspect=${inspectReady ? "ready" : "unavailable"}`,
581
+ };
582
+ }
583
+ /**
584
+ * Process-level Workspace Registry HTTP surface (2026-08-22 multi-workspace
585
+ * foundation):
586
+ * GET /api/operator/v1/workspaces — visible list
587
+ * POST /api/operator/v1/workspaces/pick — host native folder chooser
588
+ * POST /api/operator/v1/workspaces — body { "name": "<label>",
589
+ * "directoryPath": "<dir>" }
590
+ * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
591
+ *
592
+ * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
593
+ * workspace selection parameter.
594
+ *
595
+ * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
596
+ * readiness cache answer immediately (first sight may be a pending
597
+ * placeholder); probes refresh the cache in the background for subsequent
598
+ * requests. A hanging probe can never delay the 200 list.
599
+ */
600
+ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
601
+ if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
602
+ const records = await registry.listVisible();
603
+ const inspectReady = readinessDeps.inspectReady;
604
+ const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
605
+ const workspaces = records.map((record) => {
606
+ const cached = readinessCache.get(record.repoRoot);
607
+ return {
608
+ workspaceId: record.workspaceId,
609
+ name: record.name,
610
+ displayPath: record.displayPath,
611
+ repoFingerprint: record.repoFingerprint,
612
+ createdAt: record.createdAt,
613
+ lastAccessedAt: record.lastAccessedAt,
614
+ readiness: cached
615
+ ? {
616
+ state: cached.state,
617
+ summary: `pi=${cached.state}; inspect=${inspectReady ? "ready" : "unavailable"}`,
618
+ }
619
+ : pendingReadinessSummary(inspectReady),
620
+ };
621
+ });
622
+ // Respond FIRST, then refresh readiness in the background (AC-006).
623
+ sendJson(res, 200, {
624
+ ok: true,
625
+ schemaVersion: 1,
626
+ workspaces,
627
+ });
628
+ void Promise.all(records.map(async (record) => {
629
+ const cached = readinessCache.get(record.repoRoot);
630
+ if (cached && isCacheFresh(cached, Date.now()))
631
+ return;
632
+ let state;
633
+ try {
634
+ const report = await probePiReadiness({
635
+ override: deps.piReadinessProbe,
636
+ repoRoot: record.repoRoot,
637
+ });
638
+ state = report.state;
639
+ }
640
+ catch {
641
+ state = "setup-required";
642
+ }
643
+ readinessCache.set(record.repoRoot, {
644
+ state,
645
+ probedAt: Date.now(),
646
+ });
647
+ })).catch(() => undefined);
648
+ return;
649
+ }
650
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces/pick") {
651
+ const failure = evaluateMutationGate(req, {
652
+ cookieName: deps.bootToken.cookieName,
653
+ bootToken: deps.bootToken.value,
654
+ confirmationToken: deps.bootToken.confirmationToken,
655
+ consoleOrigin: deps.getConsoleOrigin(),
656
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
657
+ });
658
+ if (failure) {
659
+ sendJson(res, failure.status, {
660
+ ok: false,
661
+ error: { code: failure.code, message: failure.message },
662
+ });
663
+ return;
664
+ }
665
+ if (deps.allowNonLoopbackAccess) {
666
+ sendJson(res, 409, {
667
+ ok: false,
668
+ error: {
669
+ code: "NATIVE_PICKER_UNAVAILABLE",
670
+ message: "原生目录选择仅在本机回环地址启动的 Console 中可用。",
671
+ },
672
+ });
673
+ return;
674
+ }
675
+ const abortController = new AbortController();
676
+ const abort = () => abortController.abort();
677
+ req.once("aborted", abort);
678
+ res.once("close", abort);
679
+ try {
680
+ const directoryPath = await (deps.pickDirectory ?? pickNativeWorkspaceDirectory)(abortController.signal);
681
+ if (!res.writableEnded && !abortController.signal.aborted) {
682
+ sendJson(res, 200, { ok: true, directoryPath });
683
+ }
684
+ }
685
+ catch (error) {
686
+ if (!res.writableEnded && !abortController.signal.aborted) {
687
+ sendJson(res, 500, {
688
+ ok: false,
689
+ error: {
690
+ code: "DIRECTORY_PICKER_FAILED",
691
+ message: error instanceof Error ? error.message : String(error),
692
+ },
693
+ });
694
+ }
695
+ }
696
+ finally {
697
+ req.off("aborted", abort);
698
+ res.off("close", abort);
699
+ }
700
+ return;
701
+ }
702
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces") {
703
+ const failure = evaluateMutationGate(req, {
704
+ cookieName: deps.bootToken.cookieName,
705
+ bootToken: deps.bootToken.value,
706
+ confirmationToken: deps.bootToken.confirmationToken,
707
+ consoleOrigin: deps.getConsoleOrigin(),
708
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
709
+ });
710
+ if (failure) {
711
+ sendJson(res, failure.status, {
712
+ ok: false,
713
+ error: { code: failure.code, message: failure.message },
714
+ });
715
+ return;
716
+ }
717
+ let body;
718
+ try {
719
+ body = (await readJsonBody(req));
720
+ }
721
+ catch (error) {
722
+ sendJson(res, 400, {
723
+ ok: false,
724
+ error: {
725
+ code: "INVALID_INPUT",
726
+ message: error instanceof Error ? error.message : String(error),
727
+ },
728
+ });
729
+ return;
730
+ }
731
+ const directoryPath = typeof body.directoryPath === "string"
732
+ ? body.directoryPath
733
+ : typeof body.path === "string"
734
+ ? body.path
735
+ : "";
736
+ if (!directoryPath.trim()) {
737
+ sendJson(res, 400, {
738
+ ok: false,
739
+ error: {
740
+ code: "INVALID_INPUT",
741
+ message: "directoryPath (string) is required",
742
+ },
743
+ });
744
+ return;
745
+ }
746
+ if (body.name !== undefined &&
747
+ (typeof body.name !== "string" || !body.name.trim())) {
748
+ sendJson(res, 400, {
749
+ ok: false,
750
+ error: {
751
+ code: "INVALID_INPUT",
752
+ message: "name must be a non-empty string when provided",
753
+ },
754
+ });
755
+ return;
756
+ }
757
+ const name = typeof body.name === "string" ? body.name : undefined;
758
+ const result = await registry.add(directoryPath, name ? { name } : {});
759
+ if (!result.ok) {
760
+ const status = result.code === "DUPLICATE"
761
+ ? 409
762
+ : result.code === "INVALID_PATH"
763
+ ? 400
764
+ : 400;
765
+ sendJson(res, status, {
766
+ ok: false,
767
+ error: {
768
+ code: result.code,
769
+ message: result.message,
770
+ },
771
+ });
772
+ return;
773
+ }
774
+ const record = result.record;
775
+ sendJson(res, 201, {
776
+ ok: true,
777
+ workspace: {
778
+ workspaceId: record.workspaceId,
779
+ name: record.name,
780
+ displayPath: record.displayPath,
781
+ repoFingerprint: record.repoFingerprint,
782
+ createdAt: record.createdAt,
783
+ lastAccessedAt: record.lastAccessedAt,
784
+ readiness: {
785
+ state: "setup-required",
786
+ summary: `pi=unknown; inspect=${readinessDeps.inspectReady ? "ready" : "unavailable"}`,
787
+ },
788
+ },
789
+ });
790
+ return;
791
+ }
792
+ const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
793
+ if (method === "DELETE" && removeMatch) {
794
+ const failure = evaluateMutationGate(req, {
795
+ cookieName: deps.bootToken.cookieName,
796
+ bootToken: deps.bootToken.value,
797
+ confirmationToken: deps.bootToken.confirmationToken,
798
+ consoleOrigin: deps.getConsoleOrigin(),
799
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
800
+ });
801
+ if (failure) {
802
+ sendJson(res, failure.status, {
803
+ ok: false,
804
+ error: { code: failure.code, message: failure.message },
805
+ });
806
+ return;
807
+ }
808
+ const workspaceId = decodeURIComponent(removeMatch[1]);
809
+ const removed = await registry.remove(workspaceId);
810
+ if (!removed) {
811
+ sendJson(res, 404, {
812
+ ok: false,
813
+ error: {
814
+ code: "WORKSPACE_NOT_FOUND",
815
+ message: `unknown workspace: ${workspaceId}`,
816
+ },
817
+ });
818
+ return;
819
+ }
820
+ sendJson(res, 200, { ok: true, workspaceId, removed: true });
821
+ return;
822
+ }
823
+ sendJson(res, 404, {
824
+ ok: false,
825
+ error: { code: "NOT_FOUND", message: `unknown workspace route: ${pathname}` },
826
+ });
827
+ }
@@ -119,12 +119,12 @@ function safeEqualToken(a, b) {
119
119
  function loopbackOriginMatches(origin, consoleOrigin) {
120
120
  if (origin === consoleOrigin)
121
121
  return true;
122
- const parseLoopback = (value) => {
122
+ const parseLoopbackPort = (value) => {
123
123
  const match = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i.exec(value);
124
- return match ? `${match[1].toLowerCase()}${match[2] ?? ""}` : null;
124
+ return match ? (match[2] ?? ":80") : null;
125
125
  };
126
- const originKey = parseLoopback(origin);
127
- const consoleKey = parseLoopback(consoleOrigin);
126
+ const originKey = parseLoopbackPort(origin);
127
+ const consoleKey = parseLoopbackPort(consoleOrigin);
128
128
  return originKey !== null && consoleKey !== null && originKey === consoleKey;
129
129
  }
130
130
  export function evaluateMutationGate(req, ctx) {