@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
@@ -0,0 +1,111 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import Fastify from "fastify";
4
+ import { registerInstanceProxyRoutes } from "../http-server";
5
+ import { createRuntimeFetch } from "../../opencode/compatibility/transport";
6
+ import { rememberRuntime } from "../../opencode/compatibility/runtime";
7
+ import { LOCATION_CONTEXT_HEADER, locationRequestOptions } from "../../opencode/compatibility/location";
8
+ import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation";
9
+ import { OpenCodeSharedService } from "../../workspaces/opencode-service";
10
+ import { legacyContractFixture } from "../../opencode/compatibility/contract-fixture";
11
+ test("legacy authority precedes translation for Forms, session writes, cursors and imported history", async () => {
12
+ const upstream = Fastify();
13
+ let forwarded = 0;
14
+ upstream.all("/*", async (request) => { forwarded++; return { method: request.method, url: request.url, body: request.body, headers: request.headers }; });
15
+ await upstream.listen({ host: "127.0.0.1", port: 0 });
16
+ const endpoint = { url: `http://127.0.0.1:${upstream.server.address().port}`, auth: { type: "basic", username: "opencode", password: "fixture" } };
17
+ rememberRuntime(endpoint, { version: "2.0.3", pid: 1, discovery: "health" });
18
+ const client = { session: { get: async ({ sessionID }) => ({ location: { directory: "/native", workspaceID: sessionID === "foreign" ? "two" : "one" } }) } };
19
+ const connection = { endpoint, client, fetch: createRuntimeFetch(endpoint), assertCurrent() { }, invalidate() { }, profile: async () => "legacy" };
20
+ const directories = new Set(["/host", "/native"]);
21
+ const manager = {
22
+ get: () => ({ id: "w", path: "/host" }),
23
+ getSharedServiceEndpoint: async () => { throw new Error("Pinned request must not reacquire endpoint"); },
24
+ getSharedServiceConnection: async () => connection,
25
+ getSharedServiceClient: async () => { throw new Error("Pinned request must not reacquire client"); },
26
+ getInstanceAuthorizationHeader: () => undefined,
27
+ getServiceDirectory: () => "/native",
28
+ getServiceDirectoryForPath: async (_id, directory) => directories.has(directory) ? "/native" : undefined,
29
+ getWorktreeIdentityForPath: async (_id, directory) => directories.has(directory) ? "root" : undefined,
30
+ ownsDirectory: async (_id, directory) => directories.has(directory), ownsPath: async () => false,
31
+ ownsLocation: async (_id, location, pinned) => {
32
+ assert.equal(pinned, client);
33
+ return directories.has(location.directory) && (location.workspaceID === undefined || location.workspaceID === "one");
34
+ },
35
+ };
36
+ const app = Fastify();
37
+ registerInstanceProxyRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence(), logger: { debug() { }, error() { }, isLevelEnabled: () => false } });
38
+ const prefix = "/workspaces/w/instance";
39
+ const context = (workspaceID) => locationRequestOptions({ directory: "/host", workspaceID }).headers;
40
+ try {
41
+ const form = await app.inject({ method: "DELETE", url: `${prefix}/api/session/global/form/f`, headers: { ...context("one"), "x-opencode-directory": encodeURIComponent("/host"), cookie: "browser=secret" } });
42
+ assert.equal(form.statusCode, 200, form.body);
43
+ assert.equal(form.json().url, "/api/session/global/form/f/cancel");
44
+ assert.equal(form.json().method, "POST");
45
+ assert.equal(form.json().headers["x-opencode-directory"], encodeURIComponent("/native"));
46
+ assert.equal(form.json().headers["x-opencode-workspace"], "one");
47
+ assert.equal(form.json().headers[LOCATION_CONTEXT_HEADER], undefined);
48
+ assert.equal(form.json().headers.cookie, undefined);
49
+ const create = await app.inject({ method: "POST", url: `${prefix}/api/session`, headers: context("one"), payload: { location: { directory: "/host" } } });
50
+ assert.equal(create.statusCode, 200, create.body);
51
+ assert.deepEqual(create.json().body.location, { directory: "/native", workspaceID: "one" });
52
+ const cursor = (workspace) => Buffer.from(JSON.stringify({ directory: "/native", workspace, anchor: { id: "s", time: 1, direction: "next" } })).toString("base64url");
53
+ const listed = await app.inject(`${prefix}/api/session?cursor=${cursor("one")}&workspace=ignored&directory=/foreign`);
54
+ assert.equal(listed.statusCode, 200, listed.body);
55
+ assert.equal(new URL(listed.json().url, endpoint.url).searchParams.get("cursor"), cursor("one"));
56
+ assert.equal(new URL(listed.json().url, endpoint.url).searchParams.has("workspace"), false);
57
+ const before = forwarded;
58
+ for (const request of [
59
+ { method: "DELETE", url: `${prefix}/api/session/global/form/f`, headers: { ...context("two"), "x-opencode-directory": encodeURIComponent("/host") } },
60
+ { method: "PATCH", url: `${prefix}/api/session/foreign`, payload: { title: "forbidden" } },
61
+ { method: "GET", url: `${prefix}/api/session?cursor=${cursor("two")}` },
62
+ { method: "POST", url: `${prefix}/api/experimental/session/import`, payload: {
63
+ location: { directory: "/host", workspaceID: "one" }, info: { location: { directory: "/host", workspaceID: "one" } },
64
+ messages: [{ type: "location-switched", location: { directory: "/host", workspaceID: "two" } }],
65
+ } },
66
+ ])
67
+ assert.equal((await app.inject(request)).statusCode, 403);
68
+ assert.equal(forwarded, before, "foreign identity is refused before forwarding any mutation");
69
+ }
70
+ finally {
71
+ await app.close();
72
+ await upstream.close();
73
+ }
74
+ });
75
+ test("the first legacy-context proxy request after reconnect negotiates before authorization", async () => {
76
+ const upstream = Fastify();
77
+ const calls = [];
78
+ upstream.get("/openapi.json", async () => { calls.push("schema"); return legacyContractFixture; });
79
+ upstream.get("/api/form/request", async (request) => {
80
+ calls.push("forms");
81
+ assert.equal(request.query["location[workspace]"], "one");
82
+ return { data: [], location: { directory: "/repo", workspaceID: "one" } };
83
+ });
84
+ await upstream.listen({ host: "127.0.0.1", port: 0 });
85
+ const endpoint = { url: `http://127.0.0.1:${upstream.server.address().port}` };
86
+ rememberRuntime(endpoint, { version: "next-contract", pid: 1, discovery: "health" });
87
+ const service = new OpenCodeSharedService();
88
+ await service.client({ kind: "lifecycle", identity: "isolated-test", lifecycle: { discover: async () => endpoint, ensure: async () => endpoint } });
89
+ const app = Fastify();
90
+ registerInstanceProxyRoutes(app, { workspaceManager: {
91
+ get: () => ({ path: "/repo" }),
92
+ getSharedServiceConnection: () => service.acquire(), getSharedServiceEndpoint: async () => endpoint,
93
+ getSharedServiceClient: () => service.client(), getInstanceAuthorizationHeader: () => undefined,
94
+ getWorktreeIdentityForPath: async () => "root", ownsDirectory: async () => true, ownsPath: async () => false,
95
+ ownsLocation: async (_id, location) => {
96
+ assert.deepEqual(calls, ["schema"], "contract must be selected before profile-dependent authorization");
97
+ return location.directory === "/repo" && (location.workspaceID === undefined || location.workspaceID === "one");
98
+ },
99
+ }, worktreeDeletionFence: new WorktreeDeletionFence(), logger: { debug() { }, error() { } } });
100
+ try {
101
+ const response = await app.inject({ url: "/workspaces/w/instance/api/form?location[directory]=/repo", headers: locationRequestOptions({ directory: "/repo", workspaceID: "one" }).headers });
102
+ assert.equal(response.statusCode, 200, response.body);
103
+ assert.deepEqual(calls, ["schema", "forms"]);
104
+ }
105
+ finally {
106
+ await service.shutdown();
107
+ await app.close();
108
+ await upstream.close();
109
+ }
110
+ });
111
+ //# sourceMappingURL=instance-proxy-legacy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-proxy-legacy.test.js","sourceRoot":"","sources":["../../../src/server/__tests__/instance-proxy-legacy.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,OAAO,MAAM,SAAS,CAAA;AAG7B,OAAO,EAAE,2BAA2B,EAAsC,MAAM,gBAAgB,CAAA;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAA;AACtE,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAA;AACvG,OAAO,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAA;AACpF,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AAErF,IAAI,CAAC,+FAA+F,EAAE,KAAK,IAAI,EAAE;IAC/G,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAA;IAC1B,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;IACvJ,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IACrD,MAAM,QAAQ,GAAa,EAAE,GAAG,EAAE,oBAAqB,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAuB,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,CAAA;IAClL,eAAe,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC5E,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,SAAS,EAAyB,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAA+B,CAAA;IAChN,MAAM,UAAU,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAAE,aAAa,KAAI,CAAC,EAAE,UAAU,KAAI,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,QAAiB,EAAE,CAAA;IACzJ,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;IACjD,MAAM,OAAO,GAAkC;QAC7C,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAU;QAChD,wBAAwB,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA,CAAC,CAAC;QACvG,0BAA0B,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU;QAClD,sBAAsB,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA,CAAC,CAAC;QACnG,8BAA8B,EAAE,GAAG,EAAE,CAAC,SAAS;QAC/C,mBAAmB,EAAE,GAAG,EAAE,CAAC,SAAS;QACpC,0BAA0B,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QACxG,0BAA0B,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QACrG,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK;QAChG,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;YAC5C,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC5B,OAAO,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,WAAW,KAAK,KAAK,CAAC,CAAA;QACtH,CAAC;KACF,CAAA;IACD,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;IACrB,2BAA2B,CAAC,GAAG,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,qBAAqB,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,KAAI,CAAC,EAAE,KAAK,KAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,KAAK,EAAW,EAAE,CAAC,CAAA;IAC7L,MAAM,MAAM,GAAG,wBAAwB,CAAA;IACvC,MAAM,OAAO,GAAG,CAAC,WAAmB,EAAE,EAAE,CAAC,sBAAsB,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,CAAE,CAAC,OAAO,CAAA;IAC7G,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,MAAM,4BAA4B,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,sBAAsB,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAA;QAC9M,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,mCAAmC,CAAC,CAAA;QAClE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACxC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAA;QACxF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC,CAAA;QAChE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC,EAAE,SAAS,CAAC,CAAA;QACrE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QACnD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAA;QACzJ,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAA;QAC3F,MAAM,MAAM,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;QAC7K,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,uBAAuB,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAA;QACrH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAChG,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,CAAA;QAC3F,MAAM,MAAM,GAAG,SAAS,CAAA;QACxB,KAAK,MAAM,OAAO,IAAI;YACpB,EAAE,MAAM,EAAE,QAAiB,EAAE,GAAG,EAAE,GAAG,MAAM,4BAA4B,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,sBAAsB,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE;YAC9J,EAAE,MAAM,EAAE,OAAgB,EAAE,GAAG,EAAE,GAAG,MAAM,sBAAsB,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE;YACnG,EAAE,MAAM,EAAE,KAAc,EAAE,GAAG,EAAE,GAAG,MAAM,uBAAuB,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE;YAChF,EAAE,MAAM,EAAE,MAAe,EAAE,GAAG,EAAE,GAAG,MAAM,kCAAkC,EAAE,OAAO,EAAE;oBACpF,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE;oBACpH,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC;iBAChG,EAAE;SACJ;YAAE,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QAC5D,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,4DAA4D,CAAC,CAAA;IAC/F,CAAC;YAAS,CAAC;QAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;AACzD,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;IACxG,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAA;IAC1B,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,QAAQ,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,qBAAqB,CAAA,CAAC,CAAC,CAAC,CAAA;IACjG,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAChD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnB,MAAM,CAAC,KAAK,CAAE,OAAO,CAAC,KAAgC,CAAC,qBAAqB,CAAC,EAAE,KAAK,CAAC,CAAA;QACrF,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IACF,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IACrD,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,oBAAqB,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAuB,CAAC,IAAI,EAAE,EAAE,CAAA;IACpG,eAAe,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAA;IACpF,MAAM,OAAO,GAAG,IAAI,qBAAqB,EAAE,CAAA;IAC3C,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IACnJ,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;IACrB,2BAA2B,CAAC,GAAG,EAAE,EAAE,gBAAgB,EAAE;YACnD,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAU;YACvC,0BAA0B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,wBAAwB,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ;YACnG,sBAAsB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,8BAA8B,EAAE,GAAG,EAAE,CAAC,SAAS;YAC/F,0BAA0B,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK;YAC5G,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE;gBACpC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,kEAAkE,CAAC,CAAA;gBACvG,OAAO,QAAQ,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,WAAW,KAAK,KAAK,CAAC,CAAA;YACjH,CAAC;SACF,EAAE,qBAAqB,EAAE,IAAI,qBAAqB,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,KAAI,CAAC,EAAE,KAAK,KAAI,CAAC,EAAW,EAAE,CAAC,CAAA;IACrG,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,2DAA2D,EAAE,OAAO,EAAE,sBAAsB,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAE,CAAC,OAAO,EAAE,CAAC,CAAA;QAC7L,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACrD,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9C,CAAC;YAAS,CAAC;QAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;QAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;AACnF,CAAC,CAAC,CAAA","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport type { OpenCodeClient } from \"@opencode/client\"\nimport type { Endpoint } from \"@opencode/client/service\"\nimport { registerInstanceProxyRoutes, type InstanceProxyWorkspaceManager } from \"../http-server\"\nimport { createRuntimeFetch } from \"../../opencode/compatibility/transport\"\nimport { rememberRuntime } from \"../../opencode/compatibility/runtime\"\nimport { LOCATION_CONTEXT_HEADER, locationRequestOptions } from \"../../opencode/compatibility/location\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\nimport { OpenCodeSharedService } from \"../../workspaces/opencode-service\"\nimport { legacyContractFixture } from \"../../opencode/compatibility/contract-fixture\"\n\ntest(\"legacy authority precedes translation for Forms, session writes, cursors and imported history\", async () => {\n const upstream = Fastify()\n let forwarded = 0\n upstream.all(\"/*\", async request => { forwarded++; return { method: request.method, url: request.url, body: request.body, headers: request.headers } })\n await upstream.listen({ host: \"127.0.0.1\", port: 0 })\n const endpoint: Endpoint = { url: `http://127.0.0.1:${(upstream.server.address() as { port: number }).port}`, auth: { type: \"basic\", username: \"opencode\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.3\", pid: 1, discovery: \"health\" })\n const client = { session: { get: async ({ sessionID }: { sessionID: string }) => ({ location: { directory: \"/native\", workspaceID: sessionID === \"foreign\" ? \"two\" : \"one\" } }) } } as unknown as OpenCodeClient\n const connection = { endpoint, client, fetch: createRuntimeFetch(endpoint), assertCurrent() {}, invalidate() {}, profile: async () => \"legacy\" as const }\n const directories = new Set([\"/host\", \"/native\"])\n const manager: InstanceProxyWorkspaceManager = {\n get: () => ({ id: \"w\", path: \"/host\" }) as never,\n getSharedServiceEndpoint: async () => { throw new Error(\"Pinned request must not reacquire endpoint\") },\n getSharedServiceConnection: async () => connection,\n getSharedServiceClient: async () => { throw new Error(\"Pinned request must not reacquire client\") },\n getInstanceAuthorizationHeader: () => undefined,\n getServiceDirectory: () => \"/native\",\n getServiceDirectoryForPath: async (_id, directory) => directories.has(directory) ? \"/native\" : undefined,\n getWorktreeIdentityForPath: async (_id, directory) => directories.has(directory) ? \"root\" : undefined,\n ownsDirectory: async (_id, directory) => directories.has(directory), ownsPath: async () => false,\n ownsLocation: async (_id, location, pinned) => {\n assert.equal(pinned, client)\n return directories.has(location.directory) && (location.workspaceID === undefined || location.workspaceID === \"one\")\n },\n }\n const app = Fastify()\n registerInstanceProxyRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence(), logger: { debug() {}, error() {}, isLevelEnabled: () => false } as never })\n const prefix = \"/workspaces/w/instance\"\n const context = (workspaceID: string) => locationRequestOptions({ directory: \"/host\", workspaceID })!.headers\n try {\n const form = await app.inject({ method: \"DELETE\", url: `${prefix}/api/session/global/form/f`, headers: { ...context(\"one\"), \"x-opencode-directory\": encodeURIComponent(\"/host\"), cookie: \"browser=secret\" } })\n assert.equal(form.statusCode, 200, form.body)\n assert.equal(form.json().url, \"/api/session/global/form/f/cancel\")\n assert.equal(form.json().method, \"POST\")\n assert.equal(form.json().headers[\"x-opencode-directory\"], encodeURIComponent(\"/native\"))\n assert.equal(form.json().headers[\"x-opencode-workspace\"], \"one\")\n assert.equal(form.json().headers[LOCATION_CONTEXT_HEADER], undefined)\n assert.equal(form.json().headers.cookie, undefined)\n const create = await app.inject({ method: \"POST\", url: `${prefix}/api/session`, headers: context(\"one\"), payload: { location: { directory: \"/host\" } } })\n assert.equal(create.statusCode, 200, create.body)\n assert.deepEqual(create.json().body.location, { directory: \"/native\", workspaceID: \"one\" })\n const cursor = (workspace: string) => Buffer.from(JSON.stringify({ directory: \"/native\", workspace, anchor: { id: \"s\", time: 1, direction: \"next\" } })).toString(\"base64url\")\n const listed = await app.inject(`${prefix}/api/session?cursor=${cursor(\"one\")}&workspace=ignored&directory=/foreign`)\n assert.equal(listed.statusCode, 200, listed.body)\n assert.equal(new URL(listed.json().url, endpoint.url).searchParams.get(\"cursor\"), cursor(\"one\"))\n assert.equal(new URL(listed.json().url, endpoint.url).searchParams.has(\"workspace\"), false)\n const before = forwarded\n for (const request of [\n { method: \"DELETE\" as const, url: `${prefix}/api/session/global/form/f`, headers: { ...context(\"two\"), \"x-opencode-directory\": encodeURIComponent(\"/host\") } },\n { method: \"PATCH\" as const, url: `${prefix}/api/session/foreign`, payload: { title: \"forbidden\" } },\n { method: \"GET\" as const, url: `${prefix}/api/session?cursor=${cursor(\"two\")}` },\n { method: \"POST\" as const, url: `${prefix}/api/experimental/session/import`, payload: {\n location: { directory: \"/host\", workspaceID: \"one\" }, info: { location: { directory: \"/host\", workspaceID: \"one\" } },\n messages: [{ type: \"location-switched\", location: { directory: \"/host\", workspaceID: \"two\" } }],\n } },\n ]) assert.equal((await app.inject(request)).statusCode, 403)\n assert.equal(forwarded, before, \"foreign identity is refused before forwarding any mutation\")\n } finally { await app.close(); await upstream.close() }\n})\n\ntest(\"the first legacy-context proxy request after reconnect negotiates before authorization\", async () => {\n const upstream = Fastify()\n const calls: string[] = []\n upstream.get(\"/openapi.json\", async () => { calls.push(\"schema\"); return legacyContractFixture })\n upstream.get(\"/api/form/request\", async request => {\n calls.push(\"forms\")\n assert.equal((request.query as Record<string, string>)[\"location[workspace]\"], \"one\")\n return { data: [], location: { directory: \"/repo\", workspaceID: \"one\" } }\n })\n await upstream.listen({ host: \"127.0.0.1\", port: 0 })\n const endpoint = { url: `http://127.0.0.1:${(upstream.server.address() as { port: number }).port}` }\n rememberRuntime(endpoint, { version: \"next-contract\", pid: 1, discovery: \"health\" })\n const service = new OpenCodeSharedService()\n await service.client({ kind: \"lifecycle\", identity: \"isolated-test\", lifecycle: { discover: async () => endpoint, ensure: async () => endpoint } })\n const app = Fastify()\n registerInstanceProxyRoutes(app, { workspaceManager: {\n get: () => ({ path: \"/repo\" }) as never,\n getSharedServiceConnection: () => service.acquire(), getSharedServiceEndpoint: async () => endpoint,\n getSharedServiceClient: () => service.client(), getInstanceAuthorizationHeader: () => undefined,\n getWorktreeIdentityForPath: async () => \"root\", ownsDirectory: async () => true, ownsPath: async () => false,\n ownsLocation: async (_id, location) => {\n assert.deepEqual(calls, [\"schema\"], \"contract must be selected before profile-dependent authorization\")\n return location.directory === \"/repo\" && (location.workspaceID === undefined || location.workspaceID === \"one\")\n },\n }, worktreeDeletionFence: new WorktreeDeletionFence(), logger: { debug() {}, error() {} } as never })\n try {\n const response = await app.inject({ url: \"/workspaces/w/instance/api/form?location[directory]=/repo\", headers: locationRequestOptions({ directory: \"/repo\", workspaceID: \"one\" })!.headers })\n assert.equal(response.statusCode, 200, response.body)\n assert.deepEqual(calls, [\"schema\", \"forms\"])\n } finally { await service.shutdown(); await app.close(); await upstream.close() }\n})\n"]}
@@ -5,6 +5,7 @@ import Fastify from "fastify";
5
5
  import replyFrom from "@fastify/reply-from";
6
6
  import { redactSecrets, registerInstanceProxyRoutes } from "../http-server";
7
7
  import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation";
8
+ import { createRuntimeFetch } from "../../opencode/compatibility/transport";
8
9
  const apps = [];
9
10
  afterEach(async () => Promise.all(apps.splice(0).map((app) => app.close())));
10
11
  const cursor = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
@@ -106,6 +107,12 @@ async function harness(sessionDirectory = "/repo/worktree", activeSessions = {},
106
107
  const manager = {
107
108
  get: () => ({ id: "workspace", path: workspacePath }),
108
109
  getSharedServiceEndpoint: async () => ({ url: `http://127.0.0.1:${address.port}` }),
110
+ getSharedServiceConnection: async () => ({
111
+ endpoint: { url: `http://127.0.0.1:${address.port}` }, client,
112
+ fetch: createRuntimeFetch({ url: `http://127.0.0.1:${address.port}` }),
113
+ assertCurrent: () => { }, invalidate: () => { invalidations += 1; },
114
+ profile: async () => "modern",
115
+ }),
109
116
  invalidateSharedServiceConnection: () => { invalidations += 1; },
110
117
  getInstanceAuthorizationHeader: () => "Basic internal-secret",
111
118
  getServiceDirectory: () => serviceDirectory,
@@ -119,14 +126,13 @@ async function harness(sessionDirectory = "/repo/worktree", activeSessions = {},
119
126
  return canonical.includes("/worktree") ? "workspace:worktree" : "workspace:root";
120
127
  },
121
128
  getServicePathForPath: async (_id, candidate) => {
122
- assert.ok(pathOwnershipChecks.includes(candidate), "prompt path must be ownership-checked before translation");
129
+ assert.ok(pathOwnershipChecks.includes(candidate), "path must be ownership-checked before translation");
123
130
  servicePathCalls.push(candidate);
124
131
  return pathMappings[candidate] ?? candidate;
125
132
  },
126
133
  getSharedServiceClient: async () => client,
127
134
  ownsLocation: async (_id, location) => owned.has(location.directory)
128
- && (!location.workspaceID || location.workspaceID === (location.directory.includes("worktree") ? "worktree-location" : "owned-location")),
129
- ownsLocationWorkspace: async (_id, workspaceID) => workspaceID === "owned-location" || workspaceID === "worktree-location",
135
+ && location.workspaceID === undefined,
130
136
  ownsDirectory: async (_id, directory) => owned.has(directory),
131
137
  ownsPath: async (_id, candidate) => {
132
138
  pathOwnershipChecks.push(candidate);
@@ -153,15 +159,15 @@ async function harness(sessionDirectory = "/repo/worktree", activeSessions = {},
153
159
  describe("instance proxy location enforcement", () => {
154
160
  it("forwards native execution settlement only for a session owned by the workspace", async () => {
155
161
  const { app, requestCount, sessionGets } = await harness();
156
- const response = await app.inject({ method: "POST", url: "/workspaces/workspace/instance/api/session/session/wait" });
162
+ const response = await app.inject({ method: "POST", url: "/workspaces/workspace/instance/api/experimental/session/session/wait" });
157
163
  assert.equal(response.statusCode, 200);
158
- assert.equal(JSON.parse(response.body).url, "/api/session/session/wait");
164
+ assert.equal(JSON.parse(response.body).url, "/api/experimental/session/session/wait");
159
165
  assert.deepEqual(sessionGets, ["session"]);
160
166
  assert.equal(requestCount(), 1);
161
167
  });
162
168
  it("rejects settlement waits for sessions belonging to another workspace", async () => {
163
169
  const { app, requestCount } = await harness("/other");
164
- const response = await app.inject({ method: "POST", url: "/workspaces/workspace/instance/api/session/session/wait" });
170
+ const response = await app.inject({ method: "POST", url: "/workspaces/workspace/instance/api/experimental/session/session/wait" });
165
171
  assert.equal(response.statusCode, 403);
166
172
  assert.equal(requestCount(), 0);
167
173
  });
@@ -188,37 +194,40 @@ describe("instance proxy location enforcement", () => {
188
194
  method: "GET",
189
195
  url: "/workspaces/workspace/instance/api/session?directory=%2Fother",
190
196
  });
197
+ const activationResponse = await app.inject({
198
+ method: "POST",
199
+ url: "/workspaces/workspace/instance/api/plugin/await-activation?location%5Bdirectory%5D=%2Fother",
200
+ });
191
201
  assert.equal(bodyResponse.statusCode, 403);
192
202
  assert.equal(queryResponse.statusCode, 403);
203
+ assert.equal(activationResponse.statusCode, 403);
193
204
  assert.equal(requestCount(), 0);
194
205
  assert.doesNotMatch(bodyResponse.body, /internal-secret/);
195
206
  });
196
- it("preserves owned native workspace selectors and rejects mismatched pairs", async () => {
207
+ it("rejects removed workspace selectors even alongside owned directories", async () => {
197
208
  const { app, requestCount } = await harness();
198
209
  const query = await app.inject({
199
210
  method: "GET",
200
211
  url: "/workspaces/workspace/instance/api/model?location%5Bdirectory%5D=%2Frepo%2Fworktree&location%5Bworkspace%5D=worktree-location",
201
212
  });
202
- assert.equal(query.statusCode, 200);
203
- assert.match(JSON.parse(query.body).url, /location%5Bworkspace%5D=worktree-location/);
213
+ assert.equal(query.statusCode, 400);
204
214
  const create = await app.inject({
205
215
  method: "POST",
206
216
  url: "/workspaces/workspace/instance/api/session",
207
217
  payload: { location: { directory: "/repo", workspaceID: "owned-location" } },
208
218
  });
209
- assert.equal(create.statusCode, 200);
210
- assert.equal(JSON.parse(create.body).body.location.workspaceID, "owned-location");
219
+ assert.equal(create.statusCode, 400);
211
220
  const foreign = await app.inject({
212
221
  method: "GET",
213
222
  url: "/workspaces/workspace/instance/api/model?location%5Bdirectory%5D=%2Frepo%2Fworktree&location%5Bworkspace%5D=owned-location",
214
223
  });
215
- assert.equal(foreign.statusCode, 403);
224
+ assert.equal(foreign.statusCode, 400);
216
225
  const unsupported = await app.inject({
217
226
  method: "GET",
218
227
  url: "/workspaces/workspace/instance/api/model?workspace=owned-location",
219
228
  });
220
229
  assert.equal(unsupported.statusCode, 400);
221
- assert.equal(requestCount(), 2);
230
+ assert.equal(requestCount(), 0);
222
231
  });
223
232
  it("rejects session admission while a worktree deletion is pending", async () => {
224
233
  const { app, worktreeDeletionFence, requestCount } = await harness();
@@ -368,7 +377,7 @@ describe("instance proxy location enforcement", () => {
368
377
  const ownedCursor = cursor({ directory: "/repo/worktree", anchor: { id: "session-1", time: 1, direction: "next" } });
369
378
  const response = await app.inject({
370
379
  method: "GET",
371
- url: `/workspaces/workspace/instance/api/session?cursor=${ownedCursor}&directory=%2Fother&workspace=foreign-location`,
380
+ url: `/workspaces/workspace/instance/api/session?cursor=${ownedCursor}&directory=%2Fother`,
372
381
  });
373
382
  assert.equal(response.statusCode, 200);
374
383
  const upstreamUrl = JSON.parse(response.body).url;
@@ -391,12 +400,12 @@ describe("instance proxy location enforcement", () => {
391
400
  assert.equal((await app.inject({
392
401
  method: "GET",
393
402
  url: `/workspaces/workspace/instance/api/session?cursor=${cursor({ workspace: "foreign-location", directory: "/repo", anchor: { id: "session-1", time: 1, direction: "next" } })}`,
394
- })).statusCode, 403);
403
+ })).statusCode, 400);
395
404
  assert.equal((await app.inject({
396
405
  method: "GET",
397
406
  url: `/workspaces/workspace/instance/api/session?cursor=${cursor({ workspace: "owned-location", anchor: { id: "session-1", time: 1, direction: "next" } })}`,
398
- })).statusCode, 200);
399
- assert.equal(requestCount(), 2);
407
+ })).statusCode, 400);
408
+ assert.equal(requestCount(), 1);
400
409
  });
401
410
  it("filters PTYs and rejects foreign PTY access", async () => {
402
411
  const { app, requestCount } = await harness("/repo/worktree", {}, {}, "/repo", "/repo", {}, {
@@ -485,14 +494,14 @@ describe("instance proxy location enforcement", () => {
485
494
  });
486
495
  it("permits only native global Forms actions without session hydration", async () => {
487
496
  const { app, sessionGets, requestCount } = await harness("/other");
488
- for (const action of ["reply", "cancel"]) {
497
+ for (const [method, suffix] of [["POST", "/reply"], ["DELETE", ""]]) {
489
498
  const response = await app.inject({
490
- method: "POST",
491
- url: `/workspaces/workspace/instance/api/session/global/form/form-1/${action}`,
492
- payload: action === "reply" ? { answers: {} } : {},
499
+ method,
500
+ url: `/workspaces/workspace/instance/api/session/global/form/form-1${suffix}`,
501
+ ...(method === "POST" ? { payload: { answer: {} } } : {}),
493
502
  });
494
503
  assert.equal(response.statusCode, 200);
495
- assert.equal(JSON.parse(response.body).url, `/api/session/global/form/form-1/${action}?location%5Bdirectory%5D=%2Frepo`);
504
+ assert.equal(JSON.parse(response.body).url, `/api/session/global/form/form-1${suffix}?location%5Bdirectory%5D=%2Frepo`);
496
505
  }
497
506
  assert.deepEqual(sessionGets, []);
498
507
  assert.equal(requestCount(), 2);
@@ -503,27 +512,26 @@ describe("instance proxy location enforcement", () => {
503
512
  })).statusCode, 403);
504
513
  assert.deepEqual(sessionGets, ["global"]);
505
514
  });
506
- it("forwards a validated global Form root location and workspace", async () => {
515
+ it("forwards a validated global Form root directory", async () => {
507
516
  const { app } = await harness("/repo/worktree", {}, {}, "/repo", "/srv/repo");
508
517
  const response = await app.inject({
509
518
  method: "POST",
510
519
  url: "/workspaces/workspace/instance/api/session/global/form/form-1/reply",
511
520
  headers: {
512
521
  "x-opencode-directory": encodeURIComponent("/repo"),
513
- "x-opencode-workspace": "owned-location",
514
522
  },
515
523
  payload: { answers: {} },
516
524
  });
517
525
  assert.equal(response.statusCode, 200);
518
526
  const upstream = JSON.parse(response.body);
519
527
  assert.equal(upstream.headers["x-opencode-directory"], encodeURIComponent("/srv/repo"));
520
- assert.equal(upstream.headers["x-opencode-workspace"], "owned-location");
528
+ assert.equal(upstream.headers["x-opencode-workspace"], undefined);
521
529
  });
522
530
  it("translates and forwards a validated global Form worktree location", async () => {
523
531
  const { app } = await harness("/repo/worktree", {}, {}, "/repo", "/srv/repo", {}, {}, {}, { "/repo/worktree": "/srv/worktree" });
524
532
  const response = await app.inject({
525
- method: "POST",
526
- url: "/workspaces/workspace/instance/api/session/global/form/form-1/cancel",
533
+ method: "DELETE",
534
+ url: "/workspaces/workspace/instance/api/session/global/form/form-1",
527
535
  headers: { "x-opencode-directory": encodeURIComponent("/repo/worktree") },
528
536
  payload: {},
529
537
  });
@@ -553,7 +561,7 @@ describe("instance proxy location enforcement", () => {
553
561
  "x-opencode-workspace": "foreign-location",
554
562
  },
555
563
  payload: { answers: {} },
556
- })).statusCode, 403);
564
+ })).statusCode, 400);
557
565
  assert.equal(requestCount(), 0);
558
566
  });
559
567
  it("decodes, translates, and re-encodes Unicode global Form locations", async () => {
@@ -602,7 +610,7 @@ describe("instance proxy location enforcement", () => {
602
610
  });
603
611
  const response = await app.inject({ method: "GET", url: "/workspaces/workspace/instance/api/session/active" });
604
612
  assert.equal(response.statusCode, 200);
605
- assert.deepEqual(JSON.parse(response.body), { owned: { type: "running" } });
613
+ assert.deepEqual(JSON.parse(response.body), { data: { owned: { type: "running" } } });
606
614
  assert.deepEqual(sessionGets.sort(), ["foreign", "owned", "stale"]);
607
615
  assert.equal(requestCount(), 0);
608
616
  });
@@ -623,6 +631,9 @@ describe("instance proxy location enforcement", () => {
623
631
  })).statusCode, 403);
624
632
  assert.equal((await app.inject({ method: "GET", url: "/workspaces/workspace/instance/api/permission/saved" })).statusCode, 403);
625
633
  assert.equal((await app.inject({ method: "DELETE", url: "/workspaces/workspace/instance/api/permission/saved/global-rule" })).statusCode, 403);
634
+ for (const route of ["plugin/check", "plugin/update", "rpc/plugin/method"]) {
635
+ assert.equal((await app.inject({ method: "POST", url: `/workspaces/workspace/instance/api/${route}` })).statusCode, 403);
636
+ }
626
637
  assert.equal(requestCount(), 0);
627
638
  });
628
639
  it("allows native pending-state management for owned sessions", async () => {
@@ -632,14 +643,13 @@ describe("instance proxy location enforcement", () => {
632
643
  ["GET", "/workspaces/workspace/instance/api/session/owned/permission"],
633
644
  ["GET", "/workspaces/workspace/instance/api/session/owned/form"],
634
645
  ["POST", "/workspaces/workspace/instance/api/session/owned/background"],
635
- ["POST", "/workspaces/workspace/instance/api/session/owned/revert/clear"],
646
+ ["DELETE", "/workspaces/workspace/instance/api/session/owned/revert"],
636
647
  ["GET", "/workspaces/workspace/instance/api/skill"],
637
648
  ["GET", "/workspaces/workspace/instance/api/reference"],
638
649
  ["GET", "/workspaces/workspace/instance/api/mcp/resource"],
639
650
  ["GET", "/workspaces/workspace/instance/api/websearch/provider"],
640
651
  ["DELETE", "/workspaces/workspace/instance/api/session/owned/inbox/prompt-1"],
641
- ["POST", "/workspaces/workspace/instance/api/session/owned/inbox/prompt-1/steer"],
642
- ["POST", "/workspaces/workspace/instance/api/session/owned/inbox/prompt-1/queue"],
652
+ ["PATCH", "/workspaces/workspace/instance/api/session/owned/inbox/prompt-1"],
643
653
  ];
644
654
  for (const [method, url] of requests) {
645
655
  assert.equal((await app.inject({ method, url })).statusCode, 200, `${method} ${url}`);
@@ -703,11 +713,53 @@ describe("instance proxy location enforcement", () => {
703
713
  assert.deepEqual(servicePathCalls, Object.keys(mappings));
704
714
  assert.equal(requestCount(), 1);
705
715
  });
716
+ it("bounds filesystem list targets to owned worktrees before translating them", async () => {
717
+ const mappings = {
718
+ "/repo/sibling": "/home/dev/repo/sibling",
719
+ "/repo/worktree/src": "/home/dev/worktree/src",
720
+ };
721
+ const { app, servicePathCalls, requestCount } = await harness("/repo/worktree", {}, {}, "/repo", "/repo", mappings);
722
+ const owned = await app.inject({
723
+ method: "GET",
724
+ url: "/workspaces/workspace/instance/api/fs/list?location%5Bdirectory%5D=%2Frepo%2Fworktree&path=src",
725
+ });
726
+ assert.equal(owned.statusCode, 200);
727
+ const upstreamUrl = new URL(JSON.parse(owned.body).url, "http://upstream");
728
+ assert.equal(upstreamUrl.pathname, "/api/fs/list");
729
+ assert.equal(upstreamUrl.searchParams.get("location[directory]"), "/repo/worktree");
730
+ assert.equal(upstreamUrl.searchParams.get("path"), "/home/dev/worktree/src");
731
+ assert.deepEqual(servicePathCalls, ["/repo/worktree/src"]);
732
+ assert.equal(requestCount(), 1);
733
+ const sibling = await app.inject({
734
+ method: "GET",
735
+ url: "/workspaces/workspace/instance/api/fs/list?location%5Bdirectory%5D=%2Frepo%2Fworktree&path=..%2Fsibling",
736
+ });
737
+ assert.equal(sibling.statusCode, 200);
738
+ assert.equal(new URL(JSON.parse(sibling.body).url, "http://upstream").searchParams.get("path"), "/home/dev/repo/sibling");
739
+ for (const pathValue of ["../../../other", "/other"]) {
740
+ const foreign = await app.inject({
741
+ method: "GET",
742
+ url: `/workspaces/workspace/instance/api/fs/list?path=${encodeURIComponent(pathValue)}`,
743
+ });
744
+ assert.equal(foreign.statusCode, 403);
745
+ }
746
+ const duplicate = await app.inject({
747
+ method: "GET",
748
+ url: "/workspaces/workspace/instance/api/fs/list?path=src&path=test",
749
+ });
750
+ assert.equal(duplicate.statusCode, 400);
751
+ const nul = await app.inject({
752
+ method: "GET",
753
+ url: "/workspaces/workspace/instance/api/fs/list?path=src%00secret",
754
+ });
755
+ assert.equal(nul.statusCode, 400);
756
+ assert.equal(requestCount(), 2);
757
+ });
706
758
  it("defaults and validates only schema-defined imported session locations", async () => {
707
759
  const { app, requestCount } = await harness();
708
760
  const accepted = await app.inject({
709
761
  method: "POST",
710
- url: "/workspaces/workspace/instance/api/session/import",
762
+ url: "/workspaces/workspace/instance/api/experimental/session/import",
711
763
  payload: {
712
764
  info: { id: "session-1", metadata: { location: { directory: "/other" } } },
713
765
  messages: [{
@@ -726,9 +778,18 @@ describe("instance proxy location enforcement", () => {
726
778
  assert.deepEqual(body.messages[0].previous.location, { directory: "/repo" });
727
779
  assert.deepEqual(body.messages[0].metadata.location, { directory: "/other" });
728
780
  assert.equal(body.messages[0].content[0].state.input.location, "/other");
781
+ const obsolete = await app.inject({
782
+ method: "POST",
783
+ url: "/workspaces/workspace/instance/api/experimental/session/import",
784
+ payload: {
785
+ info: { id: "old-selector", location: { directory: "/repo", workspaceID: "owned-location" } },
786
+ messages: [],
787
+ },
788
+ });
789
+ assert.equal(obsolete.statusCode, 400);
729
790
  const rejected = await app.inject({
730
791
  method: "POST",
731
- url: "/workspaces/workspace/instance/api/session/import",
792
+ url: "/workspaces/workspace/instance/api/experimental/session/import",
732
793
  payload: {
733
794
  info: { id: "session-2", location: { directory: "/repo" } },
734
795
  messages: [{ type: "location-switched", location: { directory: "/repo/worktree" }, previous: { location: { directory: "/other" } } }],