@neuralnomads/codenomad-dev 0.20.0-dev-20260911-ea37f97b → 0.20.0-dev-20260917-aa51cbb9

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 (191) hide show
  1. package/README.md +2 -2
  2. package/dist/api-types.js.map +1 -1
  3. package/dist/opencode/automation-plugin.test.js +32 -38
  4. package/dist/opencode/automation-plugin.test.js.map +1 -1
  5. package/dist/opencode/compatibility/contract-fixture.js +15 -0
  6. package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
  7. package/dist/opencode/compatibility/events.js +9 -0
  8. package/dist/opencode/compatibility/events.js.map +1 -0
  9. package/dist/opencode/compatibility/location.js +89 -0
  10. package/dist/opencode/compatibility/location.js.map +1 -0
  11. package/dist/opencode/compatibility/location.test.js +92 -0
  12. package/dist/opencode/compatibility/location.test.js.map +1 -0
  13. package/dist/opencode/compatibility/negotiate.js +73 -0
  14. package/dist/opencode/compatibility/negotiate.js.map +1 -0
  15. package/dist/opencode/compatibility/negotiate.test.js +61 -0
  16. package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
  17. package/dist/opencode/compatibility/proxy-locations.js +137 -0
  18. package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
  19. package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
  20. package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
  21. package/dist/opencode/compatibility/proxy.js +60 -0
  22. package/dist/opencode/compatibility/proxy.js.map +1 -0
  23. package/dist/opencode/compatibility/proxy.test.js +47 -0
  24. package/dist/opencode/compatibility/proxy.test.js.map +1 -0
  25. package/dist/opencode/compatibility/requests.js +54 -0
  26. package/dist/opencode/compatibility/requests.js.map +1 -0
  27. package/dist/opencode/compatibility/runtime.js +26 -0
  28. package/dist/opencode/compatibility/runtime.js.map +1 -0
  29. package/dist/opencode/compatibility/transport.js +112 -0
  30. package/dist/opencode/compatibility/transport.js.map +1 -0
  31. package/dist/opencode/compatibility/transport.test.js +83 -0
  32. package/dist/opencode/compatibility/transport.test.js.map +1 -0
  33. package/dist/opencode/session-pruning/location.js +16 -0
  34. package/dist/opencode/session-pruning/location.js.map +1 -0
  35. package/dist/opencode/session-pruning/package.test.js +37 -0
  36. package/dist/opencode/session-pruning/package.test.js.map +1 -0
  37. package/dist/opencode/session-pruning/plugin.js +5 -2
  38. package/dist/opencode/session-pruning/plugin.js.map +1 -1
  39. package/dist/opencode/session-pruning/preview-store.js +3 -2
  40. package/dist/opencode/session-pruning/preview-store.js.map +1 -1
  41. package/dist/opencode/session-pruning/pruning.test.js +7 -5
  42. package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
  43. package/dist/opencode/session-pruning/service.js +5 -4
  44. package/dist/opencode/session-pruning/service.js.map +1 -1
  45. package/dist/opencode/session-pruning/service.test.js +65 -53
  46. package/dist/opencode/session-pruning/service.test.js.map +1 -1
  47. package/dist/permissions/opencode-replier.js +2 -2
  48. package/dist/permissions/opencode-replier.js.map +1 -1
  49. package/dist/permissions/opencode-replier.test.js +1 -1
  50. package/dist/permissions/opencode-replier.test.js.map +1 -1
  51. package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
  52. package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
  53. package/dist/plugins/session-pruning/plugin.mjs +422 -271
  54. package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
  55. package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
  56. package/dist/server/__tests__/instance-proxy.test.js +95 -34
  57. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  58. package/dist/server/http-server.js +147 -237
  59. package/dist/server/http-server.js.map +1 -1
  60. package/dist/server/routes/session-pruning.js +4 -2
  61. package/dist/server/routes/session-pruning.js.map +1 -1
  62. package/dist/server/routes/session-pruning.test.js +16 -2
  63. package/dist/server/routes/session-pruning.test.js.map +1 -1
  64. package/dist/server/routes/worktrees.js +1 -0
  65. package/dist/server/routes/worktrees.js.map +1 -1
  66. package/dist/server/routes/worktrees.test.js +54 -1
  67. package/dist/server/routes/worktrees.test.js.map +1 -1
  68. package/dist/workspaces/host-opencode-service.test.js +56 -7
  69. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  70. package/dist/workspaces/instance-client.js.map +1 -1
  71. package/dist/workspaces/instance-events.js +71 -57
  72. package/dist/workspaces/instance-events.js.map +1 -1
  73. package/dist/workspaces/instance-events.test.js +122 -9
  74. package/dist/workspaces/instance-events.test.js.map +1 -1
  75. package/dist/workspaces/manager.js +24 -39
  76. package/dist/workspaces/manager.js.map +1 -1
  77. package/dist/workspaces/manager.test.js +43 -7
  78. package/dist/workspaces/manager.test.js.map +1 -1
  79. package/dist/workspaces/opencode-cli-service.js +56 -29
  80. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  81. package/dist/workspaces/opencode-connection-generation.test.js +57 -0
  82. package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
  83. package/dist/workspaces/opencode-service.js +70 -16
  84. package/dist/workspaces/opencode-service.js.map +1 -1
  85. package/dist/workspaces/opencode-service.test.js +3 -4
  86. package/dist/workspaces/opencode-service.test.js.map +1 -1
  87. package/dist/workspaces/worktree-session-evacuation.js +31 -11
  88. package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
  89. package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
  90. package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
  91. package/dist/workspaces/wsl-opencode-service.test.js +64 -24
  92. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  93. package/package.json +3 -3
  94. package/public/assets/App-BROhwPo9.js +54 -0
  95. package/public/assets/App-BROhwPo9.js.map +1 -0
  96. package/public/assets/{FilesTab-CRpGDs1B.js → FilesTab-DY3Vr-3r.js} +3 -3
  97. package/public/assets/{FilesTab-CRpGDs1B.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
  98. package/public/assets/{GitChangesTab-DxFebTiq.js → GitChangesTab-BD6CJngE.js} +3 -3
  99. package/public/assets/{GitChangesTab-DxFebTiq.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
  100. package/public/assets/{SplitFilePanel-DgTMrrO-.js → SplitFilePanel-BdY6d_6m.js} +2 -2
  101. package/public/assets/{SplitFilePanel-DgTMrrO-.js.map → SplitFilePanel-BdY6d_6m.js.map} +1 -1
  102. package/public/assets/StatusTab-C-_2yaPa.js +2 -0
  103. package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
  104. package/public/assets/{align-justify-v_yPXSys.js → align-justify-DI8VTZ4G.js} +2 -2
  105. package/public/assets/{align-justify-v_yPXSys.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
  106. package/public/assets/{bundle-full-HzL_Jtqk.js → bundle-full-D_M65Eb_.js} +2 -2
  107. package/public/assets/{bundle-full-HzL_Jtqk.js.map → bundle-full-D_M65Eb_.js.map} +1 -1
  108. package/public/assets/{client-state-CF-PJ0GG.js → client-state-CWCzCOz6.js} +2 -2
  109. package/public/assets/{client-state-CF-PJ0GG.js.map → client-state-CWCzCOz6.js.map} +1 -1
  110. package/public/assets/{diff-viewer-BFDQBDVy.js → diff-viewer-CGPsYEr3.js} +2 -2
  111. package/public/assets/{diff-viewer-BFDQBDVy.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
  112. package/public/assets/index-B8-lAqjr.js +2 -0
  113. package/public/assets/index-B8-lAqjr.js.map +1 -0
  114. package/public/assets/index-BeUewQym.js +2 -0
  115. package/public/assets/index-BeUewQym.js.map +1 -0
  116. package/public/assets/index-BxRFHpJd.js +2 -0
  117. package/public/assets/{index-D2UHSOG5.js.map → index-BxRFHpJd.js.map} +1 -1
  118. package/public/assets/index-Bzv1kU-S.js +2 -0
  119. package/public/assets/{index-CTKYD_VJ.js.map → index-Bzv1kU-S.js.map} +1 -1
  120. package/public/assets/index-C9tI4CRG.js +2 -0
  121. package/public/assets/index-C9tI4CRG.js.map +1 -0
  122. package/public/assets/index-CGYHFZ6F.js +2 -0
  123. package/public/assets/index-CGYHFZ6F.js.map +1 -0
  124. package/public/assets/{index-CHYmKl10.js → index-CMEAjBFp.js} +2 -2
  125. package/public/assets/index-CMEAjBFp.js.map +1 -0
  126. package/public/assets/index-Cazypvyc.js +2 -0
  127. package/public/assets/index-Cazypvyc.js.map +1 -0
  128. package/public/assets/index-CwqzofWP.js +2 -0
  129. package/public/assets/index-CwqzofWP.js.map +1 -0
  130. package/public/assets/index-Da9yOa6q.js +2 -0
  131. package/public/assets/index-Da9yOa6q.js.map +1 -0
  132. package/public/assets/{index-RK9gf0uk.js → index-Ddjkirek.js} +2 -2
  133. package/public/assets/{index-RK9gf0uk.js.map → index-Ddjkirek.js.map} +1 -1
  134. package/public/assets/index-HHgvEOAH.js +3 -0
  135. package/public/assets/index-HHgvEOAH.js.map +1 -0
  136. package/public/assets/index-MeFffHqb.css +1 -0
  137. package/public/assets/loading-CdhHiSED.css +1 -0
  138. package/public/assets/{loading-BSB82snY.js → loading-Gqh6NPUr.js} +2 -2
  139. package/public/assets/{loading-BSB82snY.js.map → loading-Gqh6NPUr.js.map} +1 -1
  140. package/public/assets/main-Cbx7KdTr.js +38 -0
  141. package/public/assets/main-Cbx7KdTr.js.map +1 -0
  142. package/public/assets/{markdown-DtidoL2Z.js → markdown-BWnjyvnX.js} +4 -4
  143. package/public/assets/{markdown-DtidoL2Z.js.map → markdown-BWnjyvnX.js.map} +1 -1
  144. package/public/assets/monaco-viewer-BEJIiZWL.js +27 -0
  145. package/public/assets/monaco-viewer-BEJIiZWL.js.map +1 -0
  146. package/public/assets/{startup-state-settings-card-BY741Rt0.js → startup-state-settings-card-C4E2Itop.js} +2 -2
  147. package/public/assets/{startup-state-settings-card-BY741Rt0.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
  148. package/public/assets/{terminal-square-BdC_jwm3.js → terminal-square-C3wutdV0.js} +2 -2
  149. package/public/assets/{terminal-square-BdC_jwm3.js.map → terminal-square-C3wutdV0.js.map} +1 -1
  150. package/public/assets/tool-call-DNsAQ_m4.js +60 -0
  151. package/public/assets/tool-call-DNsAQ_m4.js.map +1 -0
  152. package/public/assets/{unified-picker-C0eo1chJ.js → unified-picker-tB7y17Qz.js} +2 -2
  153. package/public/assets/unified-picker-tB7y17Qz.js.map +1 -0
  154. package/public/assets/{wrap-text-BuMtdvMx.js → wrap-text-CkBxzt04.js} +2 -2
  155. package/public/assets/{wrap-text-BuMtdvMx.js.map → wrap-text-CkBxzt04.js.map} +1 -1
  156. package/public/index.html +4 -4
  157. package/public/loading.html +5 -5
  158. package/public/sw.js +1 -1
  159. package/public/sw.js.map +1 -1
  160. package/public/assets/App-0-FcHzPS.js +0 -44
  161. package/public/assets/App-0-FcHzPS.js.map +0 -1
  162. package/public/assets/StatusTab-LKUA9cbH.js +0 -2
  163. package/public/assets/StatusTab-LKUA9cbH.js.map +0 -1
  164. package/public/assets/index-8iPlm2r0.js +0 -2
  165. package/public/assets/index-8iPlm2r0.js.map +0 -1
  166. package/public/assets/index-B185mC7m.js +0 -2
  167. package/public/assets/index-B185mC7m.js.map +0 -1
  168. package/public/assets/index-Bbj1xF8i.js +0 -2
  169. package/public/assets/index-Bbj1xF8i.js.map +0 -1
  170. package/public/assets/index-BsmFN3k1.js +0 -2
  171. package/public/assets/index-BsmFN3k1.js.map +0 -1
  172. package/public/assets/index-Bz_OQJYA.css +0 -1
  173. package/public/assets/index-C52CfqK3.js +0 -2
  174. package/public/assets/index-C52CfqK3.js.map +0 -1
  175. package/public/assets/index-CHYmKl10.js.map +0 -1
  176. package/public/assets/index-CTKYD_VJ.js +0 -2
  177. package/public/assets/index-D2UHSOG5.js +0 -2
  178. package/public/assets/index-DDoB_Z36.js +0 -2
  179. package/public/assets/index-DDoB_Z36.js.map +0 -1
  180. package/public/assets/index-DcwAtZw-.js +0 -3
  181. package/public/assets/index-DcwAtZw-.js.map +0 -1
  182. package/public/assets/index-DxUD3RYr.js +0 -2
  183. package/public/assets/index-DxUD3RYr.js.map +0 -1
  184. package/public/assets/loading-CmEVQgyj.css +0 -1
  185. package/public/assets/main-CZztwrfv.js +0 -48
  186. package/public/assets/main-CZztwrfv.js.map +0 -1
  187. package/public/assets/monaco-viewer-CpxDfB-6.js +0 -27
  188. package/public/assets/monaco-viewer-CpxDfB-6.js.map +0 -1
  189. package/public/assets/tool-call-DbtCsJ4x.js +0 -60
  190. package/public/assets/tool-call-DbtCsJ4x.js.map +0 -1
  191. package/public/assets/unified-picker-C0eo1chJ.js.map +0 -1
@@ -7,7 +7,7 @@ import { connect as connectTcp, isIP } from "net";
7
7
  import path from "path";
8
8
  import { connect as connectTls } from "tls";
9
9
  import { fetch } from "undici";
10
- import { isInvalidRequestError, isPtyNotFoundError, isSessionNotFoundError, isShellNotFoundError } from "@opencode-ai/client";
10
+ import { isInvalidRequestError, isPtyNotFoundError, isSessionNotFoundError, isShellNotFoundError } from "@opencode/client";
11
11
  import { registerWorkspaceRoutes } from "./routes/workspaces";
12
12
  import { registerSettingsRoutes } from "./routes/settings";
13
13
  import { registerFilesystemRoutes } from "./routes/filesystem";
@@ -28,6 +28,10 @@ import { registerUsageRoutes } from "./routes/usage";
28
28
  import { registerAuthRoutes } from "./routes/auth";
29
29
  import { sendUnauthorized, wantsHtml } from "../auth/http-auth";
30
30
  import { buildPreviewRuntimeBridge, rewritePreviewImportMap, rewritePreviewJavaScriptImports } from "../previews/runtime-bridge";
31
+ import { forwardRuntimeRequest } from "../opencode/compatibility/proxy";
32
+ import { contractProfile, runtimeIdentity } from "../opencode/compatibility/runtime";
33
+ import { LOCATION_CONTEXT_HEADER, locationRequestOptions, readLocationContext } from "../opencode/compatibility/location";
34
+ import { decodeSessionListScope, prepareLocationImport, readRequestLocations } from "../opencode/compatibility/proxy-locations";
31
35
  import { createOpenCodeUpdateService } from "../opencode-update/service";
32
36
  import { WorktreeDeletionFence } from "../workspaces/worktree-session-evacuation";
33
37
  import { isAutomationPluginRequest, registerAutomationPluginRoute } from "./routes/automation-plugin";
@@ -492,11 +496,21 @@ async function proxyWorkspaceRequest(args) {
492
496
  reply.code(403).send({ error: "OpenCode route is not available through a workspace" });
493
497
  return;
494
498
  }
495
- const endpoint = await workspaceManager.getSharedServiceEndpoint(workspaceId);
499
+ const connection = await workspaceManager.getSharedServiceConnection?.(workspaceId);
500
+ const endpoint = connection?.endpoint ?? await workspaceManager.getSharedServiceEndpoint(workspaceId);
501
+ const clientForRequest = () => connection ? Promise.resolve(connection.client) : workspaceManager.getSharedServiceClient();
496
502
  if (!endpoint) {
497
503
  reply.code(502).send({ error: "OpenCode service is not ready" });
498
504
  return;
499
505
  }
506
+ const profile = connection ? await connection.profile() : contractProfile(runtimeIdentity(endpoint));
507
+ let locationContext;
508
+ try {
509
+ locationContext = readLocationContext(request.headers[LOCATION_CONTEXT_HEADER], profile);
510
+ }
511
+ catch {
512
+ return reply.code(400).send({ error: "Invalid location context" });
513
+ }
500
514
  const targetUrl = buildInstanceTargetUrl(endpoint.url, args.pathSuffix);
501
515
  if (!targetUrl) {
502
516
  reply.code(400).send({ error: "Invalid workspace instance path" });
@@ -508,22 +522,22 @@ async function proxyWorkspaceRequest(args) {
508
522
  reply.code(405).send({ error: "Method not allowed" });
509
523
  return;
510
524
  }
511
- const client = await workspaceManager.getSharedServiceClient();
525
+ const client = await clientForRequest();
512
526
  const active = await client.session.active();
513
527
  const entries = await Promise.all(Object.entries(active).map(async ([sessionId, status]) => {
514
528
  try {
515
529
  const session = await client.session.get({ sessionID: sessionId });
516
- return await workspaceManager.ownsDirectory(workspaceId, session.location.directory) ? [sessionId, status] : null;
530
+ return await workspaceManager.ownsLocation(workspaceId, session.location, client) ? [sessionId, status] : null;
517
531
  }
518
532
  catch {
519
533
  return null;
520
534
  }
521
535
  }));
522
- reply.send(Object.fromEntries(entries.filter((entry) => entry !== null)));
536
+ reply.send({ data: Object.fromEntries(entries.filter((entry) => entry !== null)) });
523
537
  return;
524
538
  }
525
539
  if (pathname.replace(/\/+$/, "") === "/api/project") {
526
- const projects = await (await workspaceManager.getSharedServiceClient()).project.list();
540
+ const projects = await (await clientForRequest()).project.list();
527
541
  const ownedProjects = await Promise.all(projects.map(async (project) => {
528
542
  if (!await workspaceManager.ownsDirectory(workspaceId, project.canonical))
529
543
  return null;
@@ -535,8 +549,8 @@ async function proxyWorkspaceRequest(args) {
535
549
  }
536
550
  const sessionListHasScope = request.method === "GET"
537
551
  && pathname.replace(/\/+$/, "") === "/api/session"
538
- && (targetUrl.searchParams.has("cursor") || targetUrl.searchParams.has("project") || targetUrl.searchParams.has("workspace"));
539
- const sessionListScope = await authorizeSessionList(targetUrl, request.method, workspaceManager, workspaceId);
552
+ && (targetUrl.searchParams.has("cursor") || targetUrl.searchParams.has("project"));
553
+ const sessionListScope = await authorizeSessionList(targetUrl, request.method, workspaceManager, workspaceId, profile, connection?.client);
540
554
  if (sessionListScope !== "allowed") {
541
555
  reply.code(sessionListScope === "invalid" ? 400 : 403).send({ error: "Session list does not belong to workspace" });
542
556
  return;
@@ -545,8 +559,7 @@ async function proxyWorkspaceRequest(args) {
545
559
  let globalFormLocation;
546
560
  if (isGlobalFormAction(pathname, request.method)) {
547
561
  const directoryHeader = request.headers["x-opencode-directory"];
548
- const workspaceHeader = request.headers["x-opencode-workspace"];
549
- if (Array.isArray(directoryHeader) || Array.isArray(workspaceHeader)) {
562
+ if (Array.isArray(directoryHeader) || request.headers["x-opencode-workspace"] !== undefined) {
550
563
  reply.code(400).send({ error: "Invalid Form location" });
551
564
  return;
552
565
  }
@@ -564,19 +577,19 @@ async function proxyWorkspaceRequest(args) {
564
577
  return;
565
578
  }
566
579
  }
567
- if (workspaceHeader !== undefined && !workspaceHeader.trim()) {
568
- reply.code(400).send({ error: "Invalid Form location" });
569
- return;
570
- }
571
- if (directory !== undefined || workspaceHeader !== undefined) {
572
- globalFormLocation = {
573
- directory: directory ?? workspace.path,
574
- ...(workspaceHeader ? { workspaceID: workspaceHeader } : {}),
575
- };
580
+ if (locationContext && locationContext.directory !== (directory ?? serviceDirectory)) {
581
+ return reply.code(400).send({ error: "Conflicting Form location" });
576
582
  }
583
+ if (directory !== undefined)
584
+ globalFormLocation = { ...locationContext, directory };
585
+ }
586
+ const imported = prepareSessionImport(pathname, request.method, stripLocationSelectors(targetUrl, request.body, workspace.path, serviceDirectory), serviceDirectory, profile);
587
+ const requestLocations = readRequestLocations(targetUrl, imported.body, workspace.path, profile);
588
+ const explicitLocations = [...requestLocations.locations];
589
+ if (locationContext) {
590
+ requestLocations.directories.push(locationContext.directory);
591
+ requestLocations.locations.push(locationContext);
577
592
  }
578
- const imported = prepareSessionImport(pathname, request.method, stripLocationSelectors(targetUrl, request.body, workspace.path, serviceDirectory), serviceDirectory);
579
- const requestLocations = readRequestDirectories(targetUrl, imported.body, workspace.path);
580
593
  if (globalFormLocation) {
581
594
  requestLocations.directories.push(globalFormLocation.directory);
582
595
  requestLocations.locations.push(globalFormLocation);
@@ -588,7 +601,7 @@ async function proxyWorkspaceRequest(args) {
588
601
  const promptFiles = readPromptFilePaths(pathname, request.method, imported.body);
589
602
  if (requestLocations.invalid
590
603
  || !(await allDirectoriesOwned(workspaceManager, workspaceId, requestLocations.directories))
591
- || !(await allLocationsOwned(workspaceManager, workspaceId, requestLocations.locations))) {
604
+ || !(await allLocationsOwned(workspaceManager, workspaceId, requestLocations.locations, connection?.client))) {
592
605
  reply.code(requestLocations.invalid ? 400 : 403).send({ error: "Location does not belong to workspace" });
593
606
  return;
594
607
  }
@@ -603,6 +616,29 @@ async function proxyWorkspaceRequest(args) {
603
616
  }
604
617
  translatedDirectories.set(directory, translated);
605
618
  }
619
+ if (locationContext && explicitLocations.some(location => translatedDirectories.get(location.directory) !== translatedDirectories.get(locationContext.directory)
620
+ || (location.workspaceID !== undefined && location.workspaceID !== locationContext.workspaceID))) {
621
+ return reply.code(400).send({ error: "Conflicting location context" });
622
+ }
623
+ const fileListPath = readFileListPath(targetUrl, request.method, requestLocations.directories[0] ?? workspace.path);
624
+ if (fileListPath.invalid) {
625
+ reply.code(400).send({ error: "Invalid filesystem path" });
626
+ return;
627
+ }
628
+ if (fileListPath.candidate) {
629
+ if (!(await workspaceManager.ownsPath(workspaceId, fileListPath.candidate))) {
630
+ reply.code(403).send({ error: "Filesystem path does not belong to workspace" });
631
+ return;
632
+ }
633
+ const translated = workspaceManager.getServicePathForPath
634
+ ? await workspaceManager.getServicePathForPath(workspaceId, fileListPath.candidate)
635
+ : fileListPath.candidate;
636
+ if (!translated) {
637
+ reply.code(403).send({ error: "Filesystem path does not belong to workspace" });
638
+ return;
639
+ }
640
+ targetUrl.searchParams.set("path", translated);
641
+ }
606
642
  const mutationIdentities = new Set();
607
643
  if (request.method !== "GET" && request.method !== "HEAD") {
608
644
  for (const directory of requestLocations.directories) {
@@ -632,9 +668,13 @@ async function proxyWorkspaceRequest(args) {
632
668
  }
633
669
  const promptBody = replacePromptFileUris(serviceBody, translatedPromptPaths);
634
670
  const requestedDirectory = requestLocations.directories[0];
635
- const runtimeLocation = { directory: requestedDirectory ? translatedDirectories.get(requestedDirectory) ?? serviceDirectory : serviceDirectory };
671
+ const runtimeLocation = {
672
+ directory: requestedDirectory ? translatedDirectories.get(requestedDirectory) ?? serviceDirectory : serviceDirectory,
673
+ workspaceID: locationContext?.workspaceID ?? explicitLocations.find(location => location.directory === requestedDirectory)?.workspaceID,
674
+ };
675
+ const runtimeLocationOptions = locationRequestOptions(runtimeLocation);
636
676
  if (pathname.replace(/\/+$/, "") === "/api/pty" && request.method === "GET") {
637
- const result = await (await workspaceManager.getSharedServiceClient()).pty.list({ location: runtimeLocation });
677
+ const result = await (await clientForRequest()).pty.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions);
638
678
  const ownership = await Promise.all(result.data.map((pty) => workspaceManager.ownsDirectory(workspaceId, pty.cwd)));
639
679
  reply.send({ ...result, data: result.data.filter((_, index) => ownership[index]) });
640
680
  return;
@@ -642,7 +682,7 @@ async function proxyWorkspaceRequest(args) {
642
682
  const ptyId = getPtyRouteId(pathname);
643
683
  if (ptyId) {
644
684
  try {
645
- const pty = await (await workspaceManager.getSharedServiceClient()).pty.get({ ptyID: ptyId, location: runtimeLocation });
685
+ const pty = await (await clientForRequest()).pty.get({ ptyID: ptyId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions);
646
686
  if (!(await workspaceManager.ownsDirectory(workspaceId, pty.data.cwd))) {
647
687
  reply.code(403).send({ error: "PTY does not belong to workspace" });
648
688
  return;
@@ -657,7 +697,7 @@ async function proxyWorkspaceRequest(args) {
657
697
  }
658
698
  }
659
699
  if (pathname.replace(/\/+$/, "") === "/api/shell" && request.method === "GET") {
660
- const result = await (await workspaceManager.getSharedServiceClient()).shell.list({ location: runtimeLocation });
700
+ const result = await (await clientForRequest()).shell.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions);
661
701
  const ownership = await Promise.all(result.data.map((shell) => workspaceManager.ownsDirectory(workspaceId, shell.cwd)));
662
702
  reply.send({ ...result, data: result.data.filter((_, index) => ownership[index]) });
663
703
  return;
@@ -665,7 +705,7 @@ async function proxyWorkspaceRequest(args) {
665
705
  const shellId = getShellRouteId(pathname);
666
706
  if (shellId) {
667
707
  try {
668
- const shell = await (await workspaceManager.getSharedServiceClient()).shell.get({ id: shellId, location: runtimeLocation });
708
+ const shell = await (await clientForRequest()).shell.get({ id: shellId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions);
669
709
  if (!(await workspaceManager.ownsDirectory(workspaceId, shell.data.cwd))) {
670
710
  reply.code(403).send({ error: "Shell does not belong to workspace" });
671
711
  return;
@@ -683,7 +723,7 @@ async function proxyWorkspaceRequest(args) {
683
723
  if (sessionId && !isGlobalFormAction(pathname, request.method)) {
684
724
  let session;
685
725
  try {
686
- session = await (await workspaceManager.getSharedServiceClient()).session.get({ sessionID: sessionId });
726
+ session = await (await clientForRequest()).session.get({ sessionID: sessionId });
687
727
  }
688
728
  catch (error) {
689
729
  if (isInvalidRequestError(error)) {
@@ -696,7 +736,7 @@ async function proxyWorkspaceRequest(args) {
696
736
  }
697
737
  throw error;
698
738
  }
699
- if (!(await workspaceManager.ownsDirectory(workspaceId, session.location.directory))) {
739
+ if (!(await workspaceManager.ownsLocation(workspaceId, session.location, await clientForRequest()))) {
700
740
  reply.code(403).send({ error: "Session does not belong to workspace" });
701
741
  return;
702
742
  }
@@ -718,14 +758,26 @@ async function proxyWorkspaceRequest(args) {
718
758
  }
719
759
  logger.debug({ workspaceId, method: request.method, targetUrl: targetUrl.toString() }, "Proxying request to instance");
720
760
  try {
761
+ connection?.assertCurrent();
762
+ if (connection || workspaceManager.getSharedServiceFetch) {
763
+ const headers = sanitizeInstanceProxyRequestHeaders(request.headers, instanceAuthHeader);
764
+ delete headers[LOCATION_CONTEXT_HEADER];
765
+ if (locationContext)
766
+ Object.assign(headers, locationRequestOptions({ ...locationContext, directory: translatedDirectories.get(locationContext.directory) }, { includeDirectory: true })?.headers);
767
+ if (globalFormLocation)
768
+ headers["x-opencode-directory"] = encodeURIComponent(translatedDirectories.get(globalFormLocation.directory));
769
+ return await forwardRuntimeRequest({
770
+ request, reply, url: targetUrl.toString(), body, headers,
771
+ fetch: connection?.fetch ?? await workspaceManager.getSharedServiceFetch(), release: releaseMutation,
772
+ invalidate: () => connection ? connection.invalidate() : workspaceManager.invalidateSharedServiceConnection?.(),
773
+ });
774
+ }
721
775
  return reply.from(targetUrl.toString(), {
722
776
  ...(body !== request.body ? { body } : {}),
723
777
  rewriteRequestHeaders: (_originalRequest, headers) => {
724
778
  const outgoingHeaders = sanitizeInstanceProxyRequestHeaders(headers, instanceAuthHeader);
725
779
  if (globalFormLocation) {
726
780
  outgoingHeaders["x-opencode-directory"] = encodeURIComponent(translatedDirectories.get(globalFormLocation.directory));
727
- if (globalFormLocation.workspaceID)
728
- outgoingHeaders["x-opencode-workspace"] = globalFormLocation.workspaceID;
729
781
  }
730
782
  if (logger.isLevelEnabled("trace")) {
731
783
  logger.trace({
@@ -776,77 +828,6 @@ function appendIncomingQuery(targetUrl, incomingUrl) {
776
828
  targetUrl.searchParams.append(key, value);
777
829
  return targetUrl;
778
830
  }
779
- function readRequestDirectories(targetUrl, body, defaultDirectory) {
780
- const directories = [];
781
- const locations = [];
782
- let invalid = false;
783
- for (const key of ["location[directory]", "directory"]) {
784
- for (const value of targetUrl.searchParams.getAll(key)) {
785
- if (value.trim())
786
- directories.push(value);
787
- else
788
- invalid = true;
789
- }
790
- }
791
- const queryWorkspaces = targetUrl.searchParams.getAll("location[workspace]");
792
- const queryDirectories = targetUrl.searchParams.getAll("location[directory]");
793
- if (queryWorkspaces.length > 1 || queryDirectories.length > 1)
794
- invalid = true;
795
- if (queryWorkspaces.length === 1) {
796
- const workspaceID = queryWorkspaces[0];
797
- if (!workspaceID.trim())
798
- invalid = true;
799
- else
800
- locations.push({ directory: queryDirectories[0] ?? defaultDirectory, workspaceID });
801
- }
802
- if (targetUrl.pathname.replace(/\/+$/, "") !== "/api/session" && targetUrl.searchParams.has("workspace"))
803
- invalid = true;
804
- if (targetUrl.searchParams.has("workspaceID") || targetUrl.searchParams.has("location[workspaceID]"))
805
- invalid = true;
806
- if (body && typeof body === "object" && !Array.isArray(body) && !Buffer.isBuffer(body)) {
807
- const input = body;
808
- if ("directory" in input) {
809
- if (typeof input.directory === "string" && input.directory.trim())
810
- directories.push(input.directory);
811
- else
812
- invalid = true;
813
- }
814
- if ("location" in input) {
815
- const location = input.location;
816
- if (location && typeof location === "object" && !Array.isArray(location)) {
817
- const source = location;
818
- const directory = source.directory;
819
- if (typeof directory === "string" && directory.trim())
820
- directories.push(directory);
821
- else
822
- invalid = true;
823
- if (source.workspace !== undefined)
824
- invalid = true;
825
- if (source.workspaceID !== undefined) {
826
- if (typeof source.workspaceID === "string" && source.workspaceID.trim() && typeof directory === "string" && directory.trim()) {
827
- locations.push({ directory, workspaceID: source.workspaceID });
828
- }
829
- else
830
- invalid = true;
831
- }
832
- }
833
- else if (location !== null && location !== undefined) {
834
- invalid = true;
835
- }
836
- }
837
- if (/^\/api\/session\/[^/]+\/move\/?$/.test(targetUrl.pathname) && "workspaceID" in input) {
838
- if (typeof input.workspaceID === "string" && input.workspaceID.trim() && typeof input.directory === "string" && input.directory.trim()) {
839
- locations.push({ directory: input.directory, workspaceID: input.workspaceID });
840
- }
841
- else
842
- invalid = true;
843
- }
844
- else if ("workspaceID" in input) {
845
- invalid = true;
846
- }
847
- }
848
- return { directories, locations, invalid };
849
- }
850
831
  function readNativeCwd(targetUrl, body, locations) {
851
832
  if (!/^\/api\/(?:shell|pty)\/?$/.test(targetUrl.pathname) || !body || typeof body !== "object" || Array.isArray(body) || Buffer.isBuffer(body))
852
833
  return;
@@ -858,11 +839,33 @@ function readNativeCwd(targetUrl, body, locations) {
858
839
  else
859
840
  locations.invalid = true;
860
841
  }
842
+ function readFileListPath(targetUrl, method, defaultDirectory) {
843
+ if (method !== "GET" || targetUrl.pathname.replace(/\/+$/, "") !== "/api/fs/list") {
844
+ return { invalid: false };
845
+ }
846
+ const values = targetUrl.searchParams.getAll("path");
847
+ if (values.length > 1)
848
+ return { invalid: true };
849
+ const requested = values[0]?.trim() || ".";
850
+ if (requested.includes("\0"))
851
+ return { invalid: true };
852
+ const windowsBase = /^[A-Za-z]:[\\/]/.test(defaultDirectory) || /^[\\/]{2}[^\\/]/.test(defaultDirectory);
853
+ if (!windowsBase && path.win32.isAbsolute(requested) && !path.posix.isAbsolute(requested)) {
854
+ return { candidate: path.win32.normalize(requested), invalid: false };
855
+ }
856
+ return {
857
+ candidate: windowsBase
858
+ ? path.win32.resolve(defaultDirectory, requested)
859
+ : path.posix.resolve(defaultDirectory, requested),
860
+ invalid: false,
861
+ };
862
+ }
861
863
  function sanitizeInstanceProxyRequestHeaders(headers, authorization) {
862
864
  const blocked = new Set([
863
865
  "authorization", "connection", "cookie", "forwarded", "host", "keep-alive", "proxy-authenticate",
864
866
  "proxy-authorization", "proxy-connection", "set-cookie", "te", "trailer", "transfer-encoding", "upgrade",
865
867
  "x-forwarded-for", "x-forwarded-host", "x-forwarded-port", "x-forwarded-proto",
868
+ LOCATION_CONTEXT_HEADER,
866
869
  ]);
867
870
  const connection = headers.connection;
868
871
  for (const name of (Array.isArray(connection) ? connection.join(",") : connection ?? "").split(","))
@@ -900,9 +903,9 @@ export function redactSecrets(value) {
900
903
  async function allDirectoriesOwned(manager, workspaceId, directories) {
901
904
  return (await Promise.all(directories.map((directory) => manager.ownsDirectory(workspaceId, directory)))).every(Boolean);
902
905
  }
903
- async function allLocationsOwned(manager, workspaceId, locations) {
904
- const unique = new Map(locations.map((location) => [`${location.directory}\0${location.workspaceID ?? ""}`, location]));
905
- return (await Promise.all([...unique.values()].map((location) => manager.ownsLocation(workspaceId, location)))).every(Boolean);
906
+ async function allLocationsOwned(manager, workspaceId, locations, client) {
907
+ const unique = new Map(locations.map((location) => [JSON.stringify([location.directory, location.workspaceID]), location]));
908
+ return (await Promise.all([...unique.values()].map((location) => manager.ownsLocation(workspaceId, location, client)))).every(Boolean);
906
909
  }
907
910
  async function allPathsOwned(manager, workspaceId, paths) {
908
911
  return (await Promise.all(paths.map((candidate) => manager.ownsPath(workspaceId, candidate)))).every(Boolean);
@@ -910,6 +913,9 @@ async function allPathsOwned(manager, workspaceId, paths) {
910
913
  function applyDefaultWorkspaceLocation(targetUrl, body, method, directory, hasLocation, sessionRoute) {
911
914
  if (hasLocation || sessionRoute)
912
915
  return body;
916
+ if (/^\/api\/(?:credential|project)(?:\/|$)/.test(targetUrl.pathname)
917
+ || /^\/api\/permission\/saved(?:\/|$)/.test(targetUrl.pathname))
918
+ return body;
913
919
  if (targetUrl.pathname === "/api/session" && method === "GET") {
914
920
  targetUrl.searchParams.set("directory", directory);
915
921
  return body;
@@ -930,99 +936,53 @@ function getSessionRouteId(pathname) {
930
936
  return match[1];
931
937
  }
932
938
  function isGlobalFormAction(pathname, method) {
933
- return method === "POST" && /^\/api\/session\/global\/form\/[^/]+\/(?:reply|cancel)\/?$/.test(pathname);
939
+ return (method === "POST" && /^\/api\/session\/global\/form\/[^/]+\/reply\/?$/.test(pathname))
940
+ || (method === "DELETE" && /^\/api\/session\/global\/form\/[^/]+\/?$/.test(pathname));
934
941
  }
935
- async function authorizeSessionList(targetUrl, method, manager, workspaceId) {
942
+ async function authorizeSessionList(targetUrl, method, manager, workspaceId, profile, client) {
936
943
  if (method !== "GET" || targetUrl.pathname.replace(/\/+$/, "") !== "/api/session")
937
944
  return "allowed";
938
945
  const cursors = targetUrl.searchParams.getAll("cursor");
939
946
  if (cursors.length > 1)
940
947
  return "invalid";
941
948
  if (cursors.length === 1) {
942
- const scope = decodeSessionListCursor(cursors[0]);
949
+ const scope = decodeSessionListScope(cursors[0], profile);
943
950
  if (!scope)
944
951
  return "invalid";
945
- for (const key of ["directory", "workspace", "location[directory]", "location[workspace]", "project", "subpath"]) {
952
+ for (const key of ["directory", "location[directory]", "project", "subpath", ...(profile === "legacy" ? ["workspace", "location[workspace]"] : [])]) {
946
953
  targetUrl.searchParams.delete(key);
947
954
  }
948
- return ownsSessionListScope(manager, workspaceId, scope);
955
+ return ownsSessionListScope(manager, workspaceId, scope, client);
949
956
  }
950
957
  const projects = targetUrl.searchParams.getAll("project");
951
958
  const subpaths = targetUrl.searchParams.getAll("subpath");
952
- const workspaces = targetUrl.searchParams.getAll("workspace");
953
959
  const directories = targetUrl.searchParams.getAll("directory");
954
- if (projects.length > 1 || subpaths.length > 1 || workspaces.length > 1 || directories.length > 1
960
+ if (projects.length > 1 || subpaths.length > 1 || directories.length > 1
955
961
  || (subpaths.length && !projects.length))
956
962
  return "invalid";
957
- const workspace = workspaces[0];
958
963
  const directory = directories[0];
959
- if ((workspace !== undefined && !workspace.trim()) || (directory !== undefined && !directory.trim()))
964
+ if (directory !== undefined && !directory.trim())
960
965
  return "invalid";
961
966
  if (!projects.length) {
962
- if (workspace && directory)
963
- return ownsSessionListScope(manager, workspaceId, { workspace, directory });
964
- if (workspace)
965
- return ownsSessionListScope(manager, workspaceId, { workspace });
966
967
  return "allowed";
967
968
  }
968
969
  const project = projects[0];
969
970
  const subpath = subpaths[0];
970
971
  if (!project || (subpath !== undefined && !isSafeRelativePath(subpath)))
971
972
  return "invalid";
972
- return ownsSessionListScope(manager, workspaceId, { ...(workspace ? { workspace } : {}), project, subpath });
973
- }
974
- function decodeSessionListCursor(cursor) {
975
- if (!cursor || !/^[A-Za-z0-9_-]+$/.test(cursor))
976
- return null;
977
- try {
978
- const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
979
- if (!value || typeof value !== "object" || Array.isArray(value))
980
- return null;
981
- const anchor = value.anchor;
982
- if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)
983
- || typeof anchor.id !== "string" || !anchor.id
984
- || typeof anchor.time !== "number" || !Number.isFinite(anchor.time)
985
- || (anchor.direction !== "previous" && anchor.direction !== "next"))
986
- return null;
987
- if (value.workspace !== undefined && (typeof value.workspace !== "string" || !value.workspace.trim()))
988
- return null;
989
- if (value.search !== undefined && typeof value.search !== "string")
990
- return null;
991
- if (value.order !== undefined && value.order !== "asc" && value.order !== "desc")
992
- return null;
993
- if (typeof value.directory === "string" && value.directory.trim() && value.project === undefined && value.subpath === undefined) {
994
- return { ...(typeof value.workspace === "string" ? { workspace: value.workspace } : {}), directory: value.directory };
995
- }
996
- if (typeof value.project === "string" && value.project.trim() && value.directory === undefined) {
997
- if (value.subpath === undefined) {
998
- return { ...(typeof value.workspace === "string" ? { workspace: value.workspace } : {}), project: value.project };
999
- }
1000
- if (typeof value.subpath === "string" && isSafeRelativePath(value.subpath)) {
1001
- return { ...(typeof value.workspace === "string" ? { workspace: value.workspace } : {}), project: value.project, subpath: value.subpath };
1002
- }
1003
- }
1004
- if (typeof value.workspace === "string" && value.directory === undefined && value.project === undefined && value.subpath === undefined) {
1005
- return { workspace: value.workspace };
1006
- }
1007
- return null;
1008
- }
1009
- catch {
1010
- return null;
1011
- }
973
+ return ownsSessionListScope(manager, workspaceId, { project, subpath, workspaceID: targetUrl.searchParams.get("workspace") ?? undefined }, client);
1012
974
  }
1013
975
  function isSafeRelativePath(value) {
1014
976
  return value === "" || (!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !value.split(/[\\/]/).includes(".."));
1015
977
  }
1016
- async function ownsSessionListScope(manager, workspaceId, scope) {
978
+ async function ownsSessionListScope(manager, workspaceId, scope, client) {
1017
979
  if (scope.directory) {
1018
- const owned = scope.workspace
1019
- ? await manager.ownsLocation(workspaceId, { directory: scope.directory, workspaceID: scope.workspace })
1020
- : await manager.ownsDirectory(workspaceId, scope.directory);
980
+ const owned = await manager.ownsLocation(workspaceId, { directory: scope.directory, workspaceID: scope.workspaceID }, client);
1021
981
  return owned ? "allowed" : "foreign";
1022
982
  }
1023
983
  if (!scope.project)
1024
- return scope.workspace && await manager.ownsLocationWorkspace(workspaceId, scope.workspace) ? "allowed" : "foreign";
1025
- const project = (await (await manager.getSharedServiceClient()).project.list()).find((candidate) => candidate.id === scope.project);
984
+ return "foreign";
985
+ const project = (await (client ?? await manager.getSharedServiceClient()).project.list()).find((candidate) => candidate.id === scope.project);
1026
986
  if (!project)
1027
987
  return "foreign";
1028
988
  const directory = scope.subpath === undefined
@@ -1030,16 +990,14 @@ async function ownsSessionListScope(manager, workspaceId, scope) {
1030
990
  : /^[A-Za-z]:[\\/]|^\\\\/.test(project.canonical)
1031
991
  ? path.win32.resolve(project.canonical, scope.subpath)
1032
992
  : path.posix.resolve(project.canonical, scope.subpath);
1033
- const owned = scope.workspace
1034
- ? await manager.ownsLocation(workspaceId, { directory, workspaceID: scope.workspace })
1035
- : await manager.ownsDirectory(workspaceId, directory);
993
+ const owned = await manager.ownsLocation(workspaceId, { directory, workspaceID: scope.workspaceID }, client);
1036
994
  return owned ? "allowed" : "foreign";
1037
995
  }
1038
996
  function getPtyRouteId(pathname) {
1039
997
  return pathname.replace(/\/+$/, "").match(/^\/api\/pty\/([^/]+)$/)?.[1] ?? null;
1040
998
  }
1041
999
  function getShellRouteId(pathname) {
1042
- return pathname.replace(/\/+$/, "").match(/^\/api\/shell\/([^/]+)(?:\/output|\/timeout)?$/)?.[1] ?? null;
1000
+ return pathname.replace(/\/+$/, "").match(/^\/api\/shell\/([^/]+)(?:\/output)?$/)?.[1] ?? null;
1043
1001
  }
1044
1002
  function buildInstanceTargetUrl(endpoint, pathSuffix) {
1045
1003
  const suffix = pathSuffix ?? "";
@@ -1078,39 +1036,42 @@ function isAllowedInstanceApiRoute(method, pathname) {
1078
1036
  ["GET", /^\/api\/(?:mcp\/resource|websearch\/provider)$/],
1079
1037
  ["GET", /^\/api\/agent\/[^/]+$/],
1080
1038
  ["GET", /^\/api\/model\/default$/],
1081
- ["GET", /^\/api\/(?:permission|question)\/request$/],
1082
- ["GET", /^\/api\/form\/request$/],
1083
- ["GET", /^\/api\/project\/current$/],
1039
+ ["GET", /^\/api\/permission\/request$/],
1040
+ ["GET", /^\/api\/form$/],
1084
1041
  ["GET", /^\/api\/project$/],
1085
1042
  ["GET", /^\/api\/vcs(?:\/status)?$/],
1086
1043
  ["GET", /^\/api\/fs\/(?:list|read\/.+)$/],
1087
- ["GET", /^\/api\/(?:pty|shell)(?:\/[^/]+(?:\/output)?)?$/],
1044
+ ["GET", /^\/api\/pty(?:\/[^/]+)?$/],
1045
+ ["GET", /^\/api\/shell(?:\/[^/]+(?:\/output)?)?$/],
1088
1046
  ["POST", /^\/api\/(?:pty|shell)$/],
1089
1047
  ["PUT", /^\/api\/pty\/[^/]+$/],
1090
1048
  ["DELETE", /^\/api\/pty\/[^/]+$/],
1091
1049
  ["DELETE", /^\/api\/shell\/[^/]+$/],
1092
- ["PATCH", /^\/api\/shell\/[^/]+\/timeout$/],
1093
- ["POST", /^\/api\/mcp\/[^/]+\/(?:connect|disconnect)$/],
1050
+ ["POST", /^\/api\/experimental\/mcp\/[^/]+\/(?:connect|disconnect)$/],
1094
1051
  ["DELETE", /^\/api\/credential\/[^/]+$/],
1095
1052
  ["POST", /^\/api\/integration\/[^/]+\/connect\/(?:key|oauth|command)$/],
1096
1053
  ["GET", /^\/api\/integration\/[^/]+\/connect\/(?:oauth|command)\/[^/]+$/],
1097
1054
  ["DELETE", /^\/api\/integration\/[^/]+\/connect\/(?:oauth|command)\/[^/]+$/],
1098
1055
  ["POST", /^\/api\/integration\/[^/]+\/connect\/oauth\/[^/]+\/complete$/],
1099
1056
  ["GET", /^\/api\/session(?:\/active)?$/],
1100
- ["POST", /^\/api\/session(?:\/import)?$/],
1057
+ ["POST", /^\/api\/session$/],
1058
+ ["POST", /^\/api\/experimental\/session\/import$/],
1101
1059
  ["GET", /^\/api\/session\/[^/]+(?:\/message(?:\/[^/]+)?)?$/],
1102
1060
  ["GET", /^\/api\/session\/[^/]+\/inbox$/],
1103
1061
  ["GET", /^\/api\/session\/[^/]+\/(?:permission|form)$/],
1104
1062
  ["DELETE", /^\/api\/session\/[^/]+$/],
1105
1063
  ["DELETE", /^\/api\/session\/[^/]+\/inbox\/[^/]+$/],
1106
- ["POST", /^\/api\/session\/[^/]+\/(?:agent|model|rename|move|prompt|command|shell|compact|interrupt|wait|background|fork)$/],
1107
- ["POST", /^\/api\/session\/[^/]+\/inbox\/[^/]+\/(?:steer|queue)$/],
1108
- ["POST", /^\/api\/session\/[^/]+\/revert\/(?:stage|clear)$/],
1109
- ["PUT", /^\/api\/session\/[^/]+\/instructions\/entries\/[^/]+$/],
1110
- ["DELETE", /^\/api\/session\/[^/]+\/instructions\/entries\/[^/]+$/],
1064
+ ["PATCH", /^\/api\/session\/[^/]+$/],
1065
+ ["POST", /^\/api\/session\/[^/]+\/(?:agent|model|move|prompt|command|shell|compact|interrupt|background|fork)$/],
1066
+ ["POST", /^\/api\/experimental\/session\/[^/]+\/wait$/],
1067
+ ["PATCH", /^\/api\/session\/[^/]+\/inbox\/[^/]+$/],
1068
+ ["POST", /^\/api\/session\/[^/]+\/revert\/stage$/],
1069
+ ["DELETE", /^\/api\/session\/[^/]+\/revert$/],
1070
+ ["PUT", /^\/api\/experimental\/session\/[^/]+\/instructions\/entries\/[^/]+$/],
1071
+ ["DELETE", /^\/api\/experimental\/session\/[^/]+\/instructions\/entries\/[^/]+$/],
1111
1072
  ["POST", /^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/],
1112
- ["POST", /^\/api\/session\/[^/]+\/question\/[^/]+\/(?:reply|reject)$/],
1113
- ["POST", /^\/api\/session\/[^/]+\/form\/[^/]+\/(?:reply|cancel)$/],
1073
+ ["POST", /^\/api\/session\/[^/]+\/form\/[^/]+\/reply$/],
1074
+ ["DELETE", /^\/api\/session\/[^/]+\/form\/[^/]+$/],
1114
1075
  ["GET", /^\/api\/experimental\/session\/[^/]+\/log$/],
1115
1076
  ];
1116
1077
  return allowed.some(([allowedMethod, pattern]) => method === allowedMethod && pattern.test(route));
@@ -1141,7 +1102,7 @@ function replaceRequestDirectories(targetUrl, body, replacements, pathname, meth
1141
1102
  input[key] = replacements.get(value);
1142
1103
  }
1143
1104
  input.location = replaceLocation(input.location);
1144
- if (pathname !== "/api/session/import" || method !== "POST")
1105
+ if (pathname !== "/api/experimental/session/import" || method !== "POST")
1145
1106
  return input;
1146
1107
  input.info = input.info && typeof input.info === "object" && !Array.isArray(input.info) && !Buffer.isBuffer(input.info)
1147
1108
  ? { ...input.info, location: replaceLocation(input.info.location) }
@@ -1267,62 +1228,11 @@ function replacePromptFileUris(body, replacements) {
1267
1228
  }),
1268
1229
  };
1269
1230
  }
1270
- function prepareSessionImport(pathname, method, body, directory) {
1231
+ function prepareSessionImport(pathname, method, body, directory, profile) {
1271
1232
  const result = { body, directories: [], locations: [], invalid: false };
1272
- if (pathname !== "/api/session/import" || method !== "POST")
1273
- return result;
1274
- if (!body || typeof body !== "object" || Array.isArray(body) || Buffer.isBuffer(body)) {
1275
- result.invalid = true;
1233
+ if (pathname !== "/api/experimental/session/import" || method !== "POST")
1276
1234
  return result;
1277
- }
1278
- const input = body;
1279
- const addLocation = (owner, key) => {
1280
- const value = owner[key];
1281
- if (value === null || value === undefined) {
1282
- owner[key] = { directory };
1283
- result.directories.push(directory);
1284
- return;
1285
- }
1286
- if (!value || typeof value !== "object" || Array.isArray(value) || Buffer.isBuffer(value)) {
1287
- result.invalid = true;
1288
- return;
1289
- }
1290
- const location = value;
1291
- if (location.directory === null || location.directory === undefined)
1292
- location.directory = directory;
1293
- if (typeof location.directory !== "string" || !location.directory.trim()) {
1294
- result.invalid = true;
1295
- return;
1296
- }
1297
- result.directories.push(location.directory);
1298
- if (location.workspace !== undefined)
1299
- result.invalid = true;
1300
- if (location.workspaceID !== undefined) {
1301
- if (typeof location.workspaceID === "string" && location.workspaceID.trim()) {
1302
- result.locations.push({ directory: location.directory, workspaceID: location.workspaceID });
1303
- }
1304
- else
1305
- result.invalid = true;
1306
- }
1307
- };
1308
- addLocation(input, "location");
1309
- if (input.info && typeof input.info === "object" && !Array.isArray(input.info) && !Buffer.isBuffer(input.info)) {
1310
- addLocation(input.info, "location");
1311
- }
1312
- if (Array.isArray(input.messages)) {
1313
- for (const value of input.messages) {
1314
- if (!value || typeof value !== "object" || Array.isArray(value) || Buffer.isBuffer(value))
1315
- continue;
1316
- const message = value;
1317
- if (message.type !== "location-switched")
1318
- continue;
1319
- addLocation(message, "location");
1320
- if (message.previous && typeof message.previous === "object" && !Array.isArray(message.previous) && !Buffer.isBuffer(message.previous)) {
1321
- addLocation(message.previous, "location");
1322
- }
1323
- }
1324
- }
1325
- return result;
1235
+ return prepareLocationImport(body, directory, profile);
1326
1236
  }
1327
1237
  function normalizeInstanceSuffix(pathSuffix) {
1328
1238
  if (!pathSuffix || pathSuffix === "/") {