@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.
- package/README.md +2 -2
- package/dist/api-types.js.map +1 -1
- package/dist/opencode/automation-plugin.test.js +32 -38
- package/dist/opencode/automation-plugin.test.js.map +1 -1
- package/dist/opencode/compatibility/contract-fixture.js +15 -0
- package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
- package/dist/opencode/compatibility/events.js +9 -0
- package/dist/opencode/compatibility/events.js.map +1 -0
- package/dist/opencode/compatibility/location.js +89 -0
- package/dist/opencode/compatibility/location.js.map +1 -0
- package/dist/opencode/compatibility/location.test.js +92 -0
- package/dist/opencode/compatibility/location.test.js.map +1 -0
- package/dist/opencode/compatibility/negotiate.js +73 -0
- package/dist/opencode/compatibility/negotiate.js.map +1 -0
- package/dist/opencode/compatibility/negotiate.test.js +61 -0
- package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
- package/dist/opencode/compatibility/proxy-locations.js +137 -0
- package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
- package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
- package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
- package/dist/opencode/compatibility/proxy.js +60 -0
- package/dist/opencode/compatibility/proxy.js.map +1 -0
- package/dist/opencode/compatibility/proxy.test.js +47 -0
- package/dist/opencode/compatibility/proxy.test.js.map +1 -0
- package/dist/opencode/compatibility/requests.js +54 -0
- package/dist/opencode/compatibility/requests.js.map +1 -0
- package/dist/opencode/compatibility/runtime.js +26 -0
- package/dist/opencode/compatibility/runtime.js.map +1 -0
- package/dist/opencode/compatibility/transport.js +112 -0
- package/dist/opencode/compatibility/transport.js.map +1 -0
- package/dist/opencode/compatibility/transport.test.js +83 -0
- package/dist/opencode/compatibility/transport.test.js.map +1 -0
- package/dist/opencode/session-pruning/location.js +16 -0
- package/dist/opencode/session-pruning/location.js.map +1 -0
- package/dist/opencode/session-pruning/package.test.js +37 -0
- package/dist/opencode/session-pruning/package.test.js.map +1 -0
- package/dist/opencode/session-pruning/plugin.js +5 -2
- package/dist/opencode/session-pruning/plugin.js.map +1 -1
- package/dist/opencode/session-pruning/preview-store.js +3 -2
- package/dist/opencode/session-pruning/preview-store.js.map +1 -1
- package/dist/opencode/session-pruning/pruning.test.js +7 -5
- package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
- package/dist/opencode/session-pruning/service.js +5 -4
- package/dist/opencode/session-pruning/service.js.map +1 -1
- package/dist/opencode/session-pruning/service.test.js +65 -53
- package/dist/opencode/session-pruning/service.test.js.map +1 -1
- package/dist/permissions/opencode-replier.js +2 -2
- package/dist/permissions/opencode-replier.js.map +1 -1
- package/dist/permissions/opencode-replier.test.js +1 -1
- package/dist/permissions/opencode-replier.test.js.map +1 -1
- package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
- package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
- package/dist/plugins/session-pruning/plugin.mjs +422 -271
- package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
- package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
- package/dist/server/__tests__/instance-proxy.test.js +95 -34
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/http-server.js +147 -237
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/routes/session-pruning.js +4 -2
- package/dist/server/routes/session-pruning.js.map +1 -1
- package/dist/server/routes/session-pruning.test.js +16 -2
- package/dist/server/routes/session-pruning.test.js.map +1 -1
- package/dist/server/routes/worktrees.js +1 -0
- package/dist/server/routes/worktrees.js.map +1 -1
- package/dist/server/routes/worktrees.test.js +54 -1
- package/dist/server/routes/worktrees.test.js.map +1 -1
- package/dist/workspaces/host-opencode-service.test.js +56 -7
- package/dist/workspaces/host-opencode-service.test.js.map +1 -1
- package/dist/workspaces/instance-client.js.map +1 -1
- package/dist/workspaces/instance-events.js +71 -57
- package/dist/workspaces/instance-events.js.map +1 -1
- package/dist/workspaces/instance-events.test.js +122 -9
- package/dist/workspaces/instance-events.test.js.map +1 -1
- package/dist/workspaces/manager.js +24 -39
- package/dist/workspaces/manager.js.map +1 -1
- package/dist/workspaces/manager.test.js +43 -7
- package/dist/workspaces/manager.test.js.map +1 -1
- package/dist/workspaces/opencode-cli-service.js +56 -29
- package/dist/workspaces/opencode-cli-service.js.map +1 -1
- package/dist/workspaces/opencode-connection-generation.test.js +57 -0
- package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
- package/dist/workspaces/opencode-service.js +70 -16
- package/dist/workspaces/opencode-service.js.map +1 -1
- package/dist/workspaces/opencode-service.test.js +3 -4
- package/dist/workspaces/opencode-service.test.js.map +1 -1
- package/dist/workspaces/worktree-session-evacuation.js +31 -11
- package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
- package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
- package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
- package/dist/workspaces/wsl-opencode-service.test.js +64 -24
- package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
- package/package.json +3 -3
- package/public/assets/App-BROhwPo9.js +54 -0
- package/public/assets/App-BROhwPo9.js.map +1 -0
- package/public/assets/{FilesTab-CRpGDs1B.js → FilesTab-DY3Vr-3r.js} +3 -3
- package/public/assets/{FilesTab-CRpGDs1B.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
- package/public/assets/{GitChangesTab-DxFebTiq.js → GitChangesTab-BD6CJngE.js} +3 -3
- package/public/assets/{GitChangesTab-DxFebTiq.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
- package/public/assets/{SplitFilePanel-DgTMrrO-.js → SplitFilePanel-BdY6d_6m.js} +2 -2
- package/public/assets/{SplitFilePanel-DgTMrrO-.js.map → SplitFilePanel-BdY6d_6m.js.map} +1 -1
- package/public/assets/StatusTab-C-_2yaPa.js +2 -0
- package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
- package/public/assets/{align-justify-v_yPXSys.js → align-justify-DI8VTZ4G.js} +2 -2
- package/public/assets/{align-justify-v_yPXSys.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
- package/public/assets/{bundle-full-HzL_Jtqk.js → bundle-full-D_M65Eb_.js} +2 -2
- package/public/assets/{bundle-full-HzL_Jtqk.js.map → bundle-full-D_M65Eb_.js.map} +1 -1
- package/public/assets/{client-state-CF-PJ0GG.js → client-state-CWCzCOz6.js} +2 -2
- package/public/assets/{client-state-CF-PJ0GG.js.map → client-state-CWCzCOz6.js.map} +1 -1
- package/public/assets/{diff-viewer-BFDQBDVy.js → diff-viewer-CGPsYEr3.js} +2 -2
- package/public/assets/{diff-viewer-BFDQBDVy.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
- package/public/assets/index-B8-lAqjr.js +2 -0
- package/public/assets/index-B8-lAqjr.js.map +1 -0
- package/public/assets/index-BeUewQym.js +2 -0
- package/public/assets/index-BeUewQym.js.map +1 -0
- package/public/assets/index-BxRFHpJd.js +2 -0
- package/public/assets/{index-D2UHSOG5.js.map → index-BxRFHpJd.js.map} +1 -1
- package/public/assets/index-Bzv1kU-S.js +2 -0
- package/public/assets/{index-CTKYD_VJ.js.map → index-Bzv1kU-S.js.map} +1 -1
- package/public/assets/index-C9tI4CRG.js +2 -0
- package/public/assets/index-C9tI4CRG.js.map +1 -0
- package/public/assets/index-CGYHFZ6F.js +2 -0
- package/public/assets/index-CGYHFZ6F.js.map +1 -0
- package/public/assets/{index-CHYmKl10.js → index-CMEAjBFp.js} +2 -2
- package/public/assets/index-CMEAjBFp.js.map +1 -0
- package/public/assets/index-Cazypvyc.js +2 -0
- package/public/assets/index-Cazypvyc.js.map +1 -0
- package/public/assets/index-CwqzofWP.js +2 -0
- package/public/assets/index-CwqzofWP.js.map +1 -0
- package/public/assets/index-Da9yOa6q.js +2 -0
- package/public/assets/index-Da9yOa6q.js.map +1 -0
- package/public/assets/{index-RK9gf0uk.js → index-Ddjkirek.js} +2 -2
- package/public/assets/{index-RK9gf0uk.js.map → index-Ddjkirek.js.map} +1 -1
- package/public/assets/index-HHgvEOAH.js +3 -0
- package/public/assets/index-HHgvEOAH.js.map +1 -0
- package/public/assets/index-MeFffHqb.css +1 -0
- package/public/assets/loading-CdhHiSED.css +1 -0
- package/public/assets/{loading-BSB82snY.js → loading-Gqh6NPUr.js} +2 -2
- package/public/assets/{loading-BSB82snY.js.map → loading-Gqh6NPUr.js.map} +1 -1
- package/public/assets/main-Cbx7KdTr.js +38 -0
- package/public/assets/main-Cbx7KdTr.js.map +1 -0
- package/public/assets/{markdown-DtidoL2Z.js → markdown-BWnjyvnX.js} +4 -4
- package/public/assets/{markdown-DtidoL2Z.js.map → markdown-BWnjyvnX.js.map} +1 -1
- package/public/assets/monaco-viewer-BEJIiZWL.js +27 -0
- package/public/assets/monaco-viewer-BEJIiZWL.js.map +1 -0
- package/public/assets/{startup-state-settings-card-BY741Rt0.js → startup-state-settings-card-C4E2Itop.js} +2 -2
- package/public/assets/{startup-state-settings-card-BY741Rt0.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
- package/public/assets/{terminal-square-BdC_jwm3.js → terminal-square-C3wutdV0.js} +2 -2
- package/public/assets/{terminal-square-BdC_jwm3.js.map → terminal-square-C3wutdV0.js.map} +1 -1
- package/public/assets/tool-call-DNsAQ_m4.js +60 -0
- package/public/assets/tool-call-DNsAQ_m4.js.map +1 -0
- package/public/assets/{unified-picker-C0eo1chJ.js → unified-picker-tB7y17Qz.js} +2 -2
- package/public/assets/unified-picker-tB7y17Qz.js.map +1 -0
- package/public/assets/{wrap-text-BuMtdvMx.js → wrap-text-CkBxzt04.js} +2 -2
- package/public/assets/{wrap-text-BuMtdvMx.js.map → wrap-text-CkBxzt04.js.map} +1 -1
- package/public/index.html +4 -4
- package/public/loading.html +5 -5
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-0-FcHzPS.js +0 -44
- package/public/assets/App-0-FcHzPS.js.map +0 -1
- package/public/assets/StatusTab-LKUA9cbH.js +0 -2
- package/public/assets/StatusTab-LKUA9cbH.js.map +0 -1
- package/public/assets/index-8iPlm2r0.js +0 -2
- package/public/assets/index-8iPlm2r0.js.map +0 -1
- package/public/assets/index-B185mC7m.js +0 -2
- package/public/assets/index-B185mC7m.js.map +0 -1
- package/public/assets/index-Bbj1xF8i.js +0 -2
- package/public/assets/index-Bbj1xF8i.js.map +0 -1
- package/public/assets/index-BsmFN3k1.js +0 -2
- package/public/assets/index-BsmFN3k1.js.map +0 -1
- package/public/assets/index-Bz_OQJYA.css +0 -1
- package/public/assets/index-C52CfqK3.js +0 -2
- package/public/assets/index-C52CfqK3.js.map +0 -1
- package/public/assets/index-CHYmKl10.js.map +0 -1
- package/public/assets/index-CTKYD_VJ.js +0 -2
- package/public/assets/index-D2UHSOG5.js +0 -2
- package/public/assets/index-DDoB_Z36.js +0 -2
- package/public/assets/index-DDoB_Z36.js.map +0 -1
- package/public/assets/index-DcwAtZw-.js +0 -3
- package/public/assets/index-DcwAtZw-.js.map +0 -1
- package/public/assets/index-DxUD3RYr.js +0 -2
- package/public/assets/index-DxUD3RYr.js.map +0 -1
- package/public/assets/loading-CmEVQgyj.css +0 -1
- package/public/assets/main-CZztwrfv.js +0 -48
- package/public/assets/main-CZztwrfv.js.map +0 -1
- package/public/assets/monaco-viewer-CpxDfB-6.js +0 -27
- package/public/assets/monaco-viewer-CpxDfB-6.js.map +0 -1
- package/public/assets/tool-call-DbtCsJ4x.js +0 -60
- package/public/assets/tool-call-DbtCsJ4x.js.map +0 -1
- 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":"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","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), "
|
|
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
|
-
&&
|
|
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("
|
|
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,
|
|
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,
|
|
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,
|
|
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(),
|
|
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
|
|
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
|
+
})).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,
|
|
399
|
-
assert.equal(requestCount(),
|
|
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
|
|
497
|
+
for (const [method, suffix] of [["POST", "/reply"], ["DELETE", ""]]) {
|
|
489
498
|
const response = await app.inject({
|
|
490
|
-
method
|
|
491
|
-
url: `/workspaces/workspace/instance/api/session/global/form/form-1
|
|
492
|
-
|
|
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
|
|
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
|
|
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"],
|
|
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: "
|
|
526
|
-
url: "/workspaces/workspace/instance/api/session/global/form/form-1
|
|
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,
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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" } } }],
|