@exagone313/dsh-podman 0.2.0-rc.2

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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/README.zh.md +41 -0
  4. package/cordis.patch.yml +17 -0
  5. package/dist/approval-reasons.d.ts +129 -0
  6. package/dist/approval-reasons.js +253 -0
  7. package/dist/approval-reasons.test.d.ts +1 -0
  8. package/dist/approval-reasons.test.js +115 -0
  9. package/dist/approval.d.ts +29 -0
  10. package/dist/approval.js +412 -0
  11. package/dist/approval.test.d.ts +1 -0
  12. package/dist/approval.test.js +462 -0
  13. package/dist/card-route.d.ts +15 -0
  14. package/dist/card-route.js +478 -0
  15. package/dist/card-route.test.d.ts +1 -0
  16. package/dist/card-route.test.js +181 -0
  17. package/dist/card-test-support.d.ts +15 -0
  18. package/dist/card-test-support.js +160 -0
  19. package/dist/client/ContainerCard.d.ts +6 -0
  20. package/dist/client/ContainerCard.js +79 -0
  21. package/dist/client/card-client.d.ts +6 -0
  22. package/dist/client/card-client.js +47 -0
  23. package/dist/client/card-protocol.d.ts +101 -0
  24. package/dist/client/card-protocol.js +10 -0
  25. package/dist/client/container-card-caches.d.ts +10 -0
  26. package/dist/client/container-card-caches.js +31 -0
  27. package/dist/client/container-card-controller.d.ts +107 -0
  28. package/dist/client/container-card-controller.js +207 -0
  29. package/dist/client/container-card-create-modal.d.ts +24 -0
  30. package/dist/client/container-card-create-modal.js +109 -0
  31. package/dist/client/container-card-directory-styles.d.ts +5 -0
  32. package/dist/client/container-card-directory-styles.js +246 -0
  33. package/dist/client/container-card-directory.d.ts +12 -0
  34. package/dist/client/container-card-directory.js +144 -0
  35. package/dist/client/container-card-editors.d.ts +30 -0
  36. package/dist/client/container-card-editors.js +116 -0
  37. package/dist/client/container-card-images.d.ts +37 -0
  38. package/dist/client/container-card-images.js +35 -0
  39. package/dist/client/container-card-paths.d.ts +20 -0
  40. package/dist/client/container-card-paths.js +88 -0
  41. package/dist/client/container-card-row.d.ts +30 -0
  42. package/dist/client/container-card-row.js +54 -0
  43. package/dist/client/container-card-secrets.d.ts +21 -0
  44. package/dist/client/container-card-secrets.js +71 -0
  45. package/dist/client/container-card-shared.d.ts +55 -0
  46. package/dist/client/container-card-shared.js +152 -0
  47. package/dist/client/container-card-styles.d.ts +21 -0
  48. package/dist/client/container-card-styles.js +143 -0
  49. package/dist/client/container-card-volumes.d.ts +12 -0
  50. package/dist/client/container-card-volumes.js +37 -0
  51. package/dist/client/container-card-workspace.d.ts +31 -0
  52. package/dist/client/container-card-workspace.js +24 -0
  53. package/dist/client/directory-picker.d.ts +12 -0
  54. package/dist/client/directory-picker.js +30 -0
  55. package/dist/client/index.d.ts +5 -0
  56. package/dist/client/index.js +4419 -0
  57. package/dist/client/locales.d.ts +9 -0
  58. package/dist/client/locales.js +370 -0
  59. package/dist/client/read-only-approval.d.ts +8 -0
  60. package/dist/client/read-only-approval.js +63 -0
  61. package/dist/client/slot-contract.d.ts +12 -0
  62. package/dist/client/slot-contract.js +4 -0
  63. package/dist/client/terminal-styles.d.ts +5 -0
  64. package/dist/client/terminal-styles.js +31 -0
  65. package/dist/client/tool-views.d.ts +8 -0
  66. package/dist/client/tool-views.js +241 -0
  67. package/dist/containers.test.d.ts +1 -0
  68. package/dist/containers.test.js +443 -0
  69. package/dist/daemons.test.d.ts +1 -0
  70. package/dist/daemons.test.js +153 -0
  71. package/dist/fs-provider.d.ts +23 -0
  72. package/dist/fs-provider.js +184 -0
  73. package/dist/fs-provider.test.d.ts +1 -0
  74. package/dist/fs-provider.test.js +260 -0
  75. package/dist/generated/version.d.ts +2 -0
  76. package/dist/generated/version.js +6 -0
  77. package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
  78. package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
  79. package/dist/grpc/runtime-client.d.ts +5 -0
  80. package/dist/grpc/runtime-client.js +43 -0
  81. package/dist/guest-rpc.d.ts +62 -0
  82. package/dist/guest-rpc.js +391 -0
  83. package/dist/images.test.d.ts +1 -0
  84. package/dist/images.test.js +79 -0
  85. package/dist/index.d.ts +22 -0
  86. package/dist/index.js +140 -0
  87. package/dist/misc.test.d.ts +1 -0
  88. package/dist/misc.test.js +141 -0
  89. package/dist/mount-enums.d.ts +22 -0
  90. package/dist/mount-enums.js +81 -0
  91. package/dist/mount-input.d.ts +14 -0
  92. package/dist/mount-input.js +64 -0
  93. package/dist/mounts.test.d.ts +1 -0
  94. package/dist/mounts.test.js +408 -0
  95. package/dist/output-reader.d.ts +34 -0
  96. package/dist/output-reader.js +87 -0
  97. package/dist/paths.test.d.ts +1 -0
  98. package/dist/paths.test.js +74 -0
  99. package/dist/preferences.d.ts +6 -0
  100. package/dist/preferences.js +27 -0
  101. package/dist/project-path.d.ts +6 -0
  102. package/dist/project-path.js +73 -0
  103. package/dist/project-path.test.d.ts +1 -0
  104. package/dist/project-path.test.js +52 -0
  105. package/dist/prompts.d.ts +13 -0
  106. package/dist/prompts.js +149 -0
  107. package/dist/public.d.ts +4 -0
  108. package/dist/public.js +63 -0
  109. package/dist/read-only-shell.d.ts +14 -0
  110. package/dist/read-only-shell.js +150 -0
  111. package/dist/read-only-shell.test.d.ts +1 -0
  112. package/dist/read-only-shell.test.js +127 -0
  113. package/dist/secrets.test.d.ts +1 -0
  114. package/dist/secrets.test.js +181 -0
  115. package/dist/settings-commands.test.d.ts +1 -0
  116. package/dist/settings-commands.test.js +549 -0
  117. package/dist/settings-create.test.d.ts +1 -0
  118. package/dist/settings-create.test.js +394 -0
  119. package/dist/settings-mounts.test.d.ts +1 -0
  120. package/dist/settings-mounts.test.js +467 -0
  121. package/dist/settings-paths.test.d.ts +1 -0
  122. package/dist/settings-paths.test.js +65 -0
  123. package/dist/settings-schema.d.ts +14 -0
  124. package/dist/settings-schema.js +14 -0
  125. package/dist/settings-workspaces.test.d.ts +1 -0
  126. package/dist/settings-workspaces.test.js +265 -0
  127. package/dist/spill-store.d.ts +17 -0
  128. package/dist/spill-store.js +54 -0
  129. package/dist/spill-store.test.d.ts +1 -0
  130. package/dist/spill-store.test.js +59 -0
  131. package/dist/subprocess.d.ts +9 -0
  132. package/dist/subprocess.js +369 -0
  133. package/dist/subprocess.test.d.ts +1 -0
  134. package/dist/subprocess.test.js +234 -0
  135. package/dist/test-support.d.ts +328 -0
  136. package/dist/test-support.js +437 -0
  137. package/dist/tool-defs.d.ts +26 -0
  138. package/dist/tool-defs.js +188 -0
  139. package/dist/tool-handlers.d.ts +2 -0
  140. package/dist/tool-handlers.js +546 -0
  141. package/dist/tool-params.d.ts +803 -0
  142. package/dist/tool-params.js +494 -0
  143. package/dist/tool-schemas.d.ts +2 -0
  144. package/dist/tool-schemas.js +5 -0
  145. package/dist/tool-views.d.ts +2 -0
  146. package/dist/tool-views.js +56 -0
  147. package/dist/views.test.d.ts +1 -0
  148. package/dist/views.test.js +23 -0
  149. package/dist/volumes.test.d.ts +1 -0
  150. package/dist/volumes.test.js +40 -0
  151. package/dist/workspace-binding.d.ts +40 -0
  152. package/dist/workspace-binding.js +275 -0
  153. package/dist/workspace-binding.test.d.ts +1 -0
  154. package/dist/workspace-binding.test.js +337 -0
  155. package/docs/architecture.md +173 -0
  156. package/docs/architecture.zh.md +129 -0
  157. package/docs/configuration.md +160 -0
  158. package/docs/configuration.zh.md +147 -0
  159. package/docs/development.md +142 -0
  160. package/docs/development.zh.md +135 -0
  161. package/docs/install-dsh-and-dsh-podman.md +165 -0
  162. package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
  163. package/docs/usage.md +393 -0
  164. package/docs/usage.zh.md +315 -0
  165. package/package.json +119 -0
@@ -0,0 +1,265 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { test } from "node:test";
5
+ import { strict as assert } from "node:assert";
6
+ import { baseValue, fakeContext, fakeScope } from "./card-test-support.js";
7
+ import { installCardCommandDriver } from "./card-test-support.js";
8
+ import { WORKSPACE_ID } from "./test-support.js";
9
+ test("refresh on install publishes containers, images and workspaces", async () => {
10
+ const scope = fakeScope(baseValue());
11
+ const calls = [];
12
+ const resolver = {
13
+ getConfig: () => ({
14
+ defaultImage: "archlinux",
15
+ socketsRoot: "/run/dsh-podman",
16
+ projectsRoot: "/projects",
17
+ }),
18
+ setConfig: () => { },
19
+ async control(method, request) {
20
+ calls.push([method, request]);
21
+ if (method === "listContainers") {
22
+ return { containers: [{ containerName: "c1", workspaceSlug: "w1" }] };
23
+ }
24
+ if (method === "listImages") {
25
+ return { images: [{ imageId: "img1" }] };
26
+ }
27
+ if (method === "listWorkspaces") {
28
+ return { workspaces: [{ workspaceSlug: "w1", mounts: [{ projectName: "team/app" }] }] };
29
+ }
30
+ if (method === "listVolumes") {
31
+ return { volumes: [{ name: "data" }, { name: "cache" }] };
32
+ }
33
+ if (method === "listSecrets") {
34
+ return { secrets: [{ name: "db-pass" }, { name: "api-key" }] };
35
+ }
36
+ return {};
37
+ },
38
+ };
39
+ await installCardCommandDriver(fakeContext(scope), resolver);
40
+ await scope.update({}); // settle the queued async refresh
41
+ assert.deepEqual(calls.map(([method]) => method), ["listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
42
+ assert.equal(scope.value.containers.length, 1);
43
+ assert.equal(scope.value.images.length, 1);
44
+ const workspaces = scope.value.workspaces;
45
+ assert.equal(workspaces.length, 1);
46
+ assert.equal(workspaces[0].workspaceSlug, "w1");
47
+ assert.equal(workspaces[0].projectName, "team/app");
48
+ assert.deepEqual(scope.value.volumes, [{ name: "data" }, { name: "cache" }]);
49
+ assert.deepEqual(scope.value.secrets, [{ name: "db-pass" }, { name: "api-key" }]);
50
+ });
51
+ test("handle never writes projectsRoot into setConfig", async () => {
52
+ const scope = fakeScope(baseValue());
53
+ const setConfigCalls = [];
54
+ const resolver = {
55
+ getConfig: () => ({}),
56
+ setConfig: (patch) => setConfigCalls.push(patch),
57
+ async control(method) {
58
+ if (method === "listContainers")
59
+ return { containers: [] };
60
+ if (method === "listImages")
61
+ return { images: [] };
62
+ if (method === "listWorkspaces")
63
+ return { workspaces: [] };
64
+ return {};
65
+ },
66
+ };
67
+ await installCardCommandDriver(fakeContext(scope), resolver);
68
+ await scope.update({});
69
+ await new Promise((resolve) => setImmediate(resolve));
70
+ assert.ok(setConfigCalls.length > 0, "handle must write resolver config");
71
+ for (const patch of setConfigCalls) {
72
+ assert.ok(!Object.prototype.hasOwnProperty.call(patch, "projectsRoot"), "projectsRoot must not be written via setConfig");
73
+ }
74
+ });
75
+ test("settings seed the image and sockets root from the resolver config", async () => {
76
+ let registeredBase;
77
+ const scope = fakeScope(baseValue());
78
+ const context = {
79
+ inject(deps, callback) {
80
+ assert.deepEqual(deps, ["settings"]);
81
+ callback({
82
+ settings: {
83
+ register(_namespace, _schema, options) {
84
+ registeredBase = options?.base ?? {};
85
+ return scope;
86
+ },
87
+ },
88
+ });
89
+ },
90
+ };
91
+ const resolver = {
92
+ getConfig: () => ({
93
+ defaultImage: "archlinux",
94
+ socketsRoot: "/run/dsh-podman",
95
+ projectsRoot: "/projects",
96
+ }),
97
+ setConfig: () => { },
98
+ async control() {
99
+ return {};
100
+ },
101
+ };
102
+ await installCardCommandDriver(context, resolver);
103
+ assert.equal(registeredBase?.defaultImage, "archlinux");
104
+ assert.equal(registeredBase?.socketsRoot, "/run/dsh-podman");
105
+ });
106
+ test("create command drives createWorkspace with env", async () => {
107
+ const scope = fakeScope(baseValue());
108
+ const calls = [];
109
+ const resolver = {
110
+ getConfig: () => ({}),
111
+ setConfig: () => { },
112
+ async control(method, request) {
113
+ calls.push([method, request]);
114
+ if (method === "listContainers")
115
+ return { containers: [] };
116
+ if (method === "listImages")
117
+ return { images: [] };
118
+ if (method === "listWorkspaces")
119
+ return { workspaces: [] };
120
+ return {};
121
+ },
122
+ };
123
+ await installCardCommandDriver(fakeContext(scope), resolver);
124
+ await scope.update({
125
+ command: {
126
+ op: "create",
127
+ workspace: "w1",
128
+ projectName: "w1",
129
+ image: "img1",
130
+ at: 13,
131
+ mounts: [],
132
+ env: { A: "1", B: "2" },
133
+ container: "",
134
+ secretEnvMap: {},
135
+ mount: null,
136
+ },
137
+ });
138
+ await new Promise((resolve) => setImmediate(resolve));
139
+ const createCall = calls.find(([method]) => method === "createWorkspace");
140
+ assert.deepEqual(createCall?.[1], {
141
+ workspaceSlug: "w1",
142
+ projectName: "w1",
143
+ imageId: "img1",
144
+ env: { A: "1", B: "2" },
145
+ });
146
+ assert.equal(scope.value.command, null);
147
+ });
148
+ test("create command drives createWorkspace without env when empty", async () => {
149
+ const scope = fakeScope(baseValue());
150
+ const calls = [];
151
+ const resolver = {
152
+ getConfig: () => ({}),
153
+ setConfig: () => { },
154
+ async control(method, request) {
155
+ calls.push([method, request]);
156
+ if (method === "listContainers")
157
+ return { containers: [] };
158
+ if (method === "listImages")
159
+ return { images: [] };
160
+ if (method === "listWorkspaces")
161
+ return { workspaces: [] };
162
+ return {};
163
+ },
164
+ };
165
+ await installCardCommandDriver(fakeContext(scope), resolver);
166
+ await scope.update({
167
+ command: {
168
+ op: "create",
169
+ workspace: "w1",
170
+ projectName: "w1",
171
+ image: "img1",
172
+ at: 14,
173
+ mounts: [],
174
+ env: {},
175
+ container: "",
176
+ secretEnvMap: {},
177
+ mount: null,
178
+ },
179
+ });
180
+ await new Promise((resolve) => setImmediate(resolve));
181
+ const createCall = calls.find(([method]) => method === "createWorkspace");
182
+ assert.deepEqual(createCall?.[1], {
183
+ workspaceSlug: "w1",
184
+ projectName: "w1",
185
+ imageId: "img1",
186
+ });
187
+ assert.equal(scope.value.command, null);
188
+ });
189
+ test("workspace list comes from the dsh registry even without orchestrator state", async () => {
190
+ const scope = fakeScope(baseValue());
191
+ const registry = {
192
+ list: () => [
193
+ { id: WORKSPACE_ID, path: "/projects/team/app", title: "app", createdAt: "2026-01-01T00:00:00Z" },
194
+ ],
195
+ };
196
+ const resolver = {
197
+ getConfig: () => ({
198
+ defaultImage: "archlinux",
199
+ socketsRoot: "/run/dsh-podman",
200
+ projectsRoot: "/projects",
201
+ }),
202
+ setConfig: () => { },
203
+ async control(method) {
204
+ if (method === "listContainers")
205
+ return { containers: [] };
206
+ if (method === "listImages")
207
+ return { images: [] };
208
+ if (method === "listWorkspaces")
209
+ return { workspaces: [] };
210
+ return {};
211
+ },
212
+ };
213
+ await installCardCommandDriver(fakeContext(scope), resolver, registry);
214
+ await scope.update({});
215
+ const workspaces = scope.value.workspaces;
216
+ assert.equal(workspaces.length, 1);
217
+ assert.equal(workspaces[0].projectName, "team/app");
218
+ assert.equal(workspaces[0].containerName, "");
219
+ assert.deepEqual(workspaces[0].mounts, [
220
+ { projectName: "team/app", mode: "MOUNT_MODE_READ_WRITE" },
221
+ ]);
222
+ assert.ok(workspaces[0].workspaceSlug.length > 0);
223
+ });
224
+ test("dsh workspace layers orchestrator container info", async () => {
225
+ const scope = fakeScope(baseValue());
226
+ const registry = {
227
+ list: () => [
228
+ { id: WORKSPACE_ID, path: "/projects/team/app", title: "app", createdAt: "2026-01-01T00:00:00Z" },
229
+ ],
230
+ };
231
+ const resolver = {
232
+ getConfig: () => ({
233
+ defaultImage: "archlinux",
234
+ socketsRoot: "/run/dsh-podman",
235
+ projectsRoot: "/projects",
236
+ }),
237
+ setConfig: () => { },
238
+ async control(method) {
239
+ if (method === "listContainers")
240
+ return { containers: [] };
241
+ if (method === "listImages")
242
+ return { images: [] };
243
+ if (method === "listWorkspaces") {
244
+ return {
245
+ workspaces: [{
246
+ workspaceSlug: WORKSPACE_ID,
247
+ containerName: `dsh-podman-${WORKSPACE_ID}-default`,
248
+ imageId: "arch",
249
+ status: "running",
250
+ mounts: [{ projectName: "team/app", mode: "MOUNT_MODE_READ_WRITE" }],
251
+ }],
252
+ };
253
+ }
254
+ return {};
255
+ },
256
+ };
257
+ await installCardCommandDriver(fakeContext(scope), resolver, registry);
258
+ await scope.update({});
259
+ const workspaces = scope.value.workspaces;
260
+ assert.equal(workspaces.length, 1);
261
+ assert.equal(workspaces[0].projectName, "team/app");
262
+ assert.equal(workspaces[0].containerName, `dsh-podman-${WORKSPACE_ID}-default`);
263
+ assert.equal(workspaces[0].status, "running");
264
+ assert.equal(workspaces[0].imageId, "arch");
265
+ });
@@ -0,0 +1,17 @@
1
+ import type { WorkspaceResolver } from "./workspace-binding.js";
2
+ export interface SpillRequest {
3
+ owner: {
4
+ sessionId: string;
5
+ };
6
+ source: unknown;
7
+ suggestedName: string;
8
+ content: string;
9
+ }
10
+ export interface SpillRef {
11
+ locator: string;
12
+ bytes: number;
13
+ retrievalHint: string;
14
+ }
15
+ export declare function createSpillStore(ctx: any, resolver: WorkspaceResolver): {
16
+ saveText(input: SpillRequest): Promise<SpillRef>;
17
+ };
@@ -0,0 +1,54 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { randomUUID } from "node:crypto";
5
+ import { unaryGuest, writeGuestFile } from "./guest-rpc.js";
6
+ import { SPILL_ROOT } from "./output-reader.js";
7
+ // Tool-result spills live in a subdirectory of the guest's scratch root so they
8
+ // never collide with the command-output spills the guest writes directly there.
9
+ const TOOL_SPILL_ROOT = `${SPILL_ROOT}/spill`;
10
+ // spillName turns a caller-suggested base name into one safe, collision-free
11
+ // path segment. The suggestion is a hint, never a path.
12
+ function spillName(suggestedName) {
13
+ const safe = String(suggestedName ?? "")
14
+ .replace(/[^A-Za-z0-9._-]/g, "_")
15
+ .replace(/\.{2,}/g, "_")
16
+ .replace(/^[.-]+/, "")
17
+ .slice(0, 64);
18
+ return `${randomUUID().replace(/-/g, "")}-${safe === "" ? "spill.txt" : safe}`;
19
+ }
20
+ // sessionSegment is the per-session directory name: session ids are UUIDs, but
21
+ // a backend must never trust a caller-supplied segment in a path.
22
+ function sessionSegment(sessionId) {
23
+ return sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
24
+ }
25
+ // createSpillStore backs `ctx.spillStore` with the session's container:
26
+ // oversized tool results are written inside the guest, and the locator is the
27
+ // container path, so the agent reads them back with `container_read` /
28
+ // `container_grep` (the host filesystem is not reachable from a container).
29
+ export function createSpillStore(ctx, resolver) {
30
+ return {
31
+ async saveText(input) {
32
+ const sessionId = String(input?.owner?.sessionId ?? "");
33
+ const session = ctx.get("sessions")?.get?.(sessionId);
34
+ const cwd = session?.header?.cwd;
35
+ if (typeof cwd !== "string" || cwd === "") {
36
+ throw new Error(`cannot spill a result for session "${sessionId}": no session working directory`);
37
+ }
38
+ const binding = await resolver.resolve(cwd);
39
+ const directory = `${TOOL_SPILL_ROOT}/${sessionSegment(sessionId)}`;
40
+ await unaryGuest({ targetKey: directory, displayPath: directory, binding }, "mkdir", { path: directory, parents: true });
41
+ const path = `${directory}/${spillName(input.suggestedName)}`;
42
+ const content = String(input.content ?? "");
43
+ await writeGuestFile(binding, path, content, {
44
+ create: true,
45
+ truncate: true,
46
+ });
47
+ return {
48
+ locator: path,
49
+ bytes: Buffer.byteLength(content, "utf8"),
50
+ retrievalHint: "Use container_read with offset/limit, or container_grep this path to search within it.",
51
+ };
52
+ },
53
+ };
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { createSpillStore } from "./spill-store.js";
7
+ import { guestFileRecorder } from "./test-support.js";
8
+ const SESSION_ID = "2c573001-4171-4900-904b-12a5cc02737a";
9
+ function fakeCtx(sessions) {
10
+ return {
11
+ get: (name) => name === "sessions"
12
+ ? { get: (id) => sessions[id] }
13
+ : undefined,
14
+ };
15
+ }
16
+ function withSession() {
17
+ return fakeCtx({ [SESSION_ID]: { header: { cwd: "/projects/team" } } });
18
+ }
19
+ test("saveText writes a tool-result spill inside the session container", async () => {
20
+ const { writes, mkdirs, resolver } = guestFileRecorder();
21
+ const store = createSpillStore(withSession(), resolver);
22
+ const content = "x".repeat(60000);
23
+ const ref = await store.saveText({
24
+ owner: { sessionId: SESSION_ID },
25
+ source: { kind: "tool", toolName: "web_fetch", callId: "c1", label: "result" },
26
+ suggestedName: "web_fetch.txt",
27
+ content,
28
+ });
29
+ assert.equal(mkdirs[0], `/tmp/dsh-podman/spill/${SESSION_ID}`);
30
+ assert.equal(writes.length, 1);
31
+ assert.match(writes[0].path, new RegExp(`^/tmp/dsh-podman/spill/${SESSION_ID}/[0-9a-f]{32}-web_fetch\\.txt$`));
32
+ assert.equal(ref.locator, writes[0].path);
33
+ assert.equal(ref.bytes, content.length);
34
+ assert.equal(writes[0].content, content, "the full text is persisted verbatim");
35
+ assert.match(ref.retrievalHint, /container_read/);
36
+ });
37
+ test("saveText never lets the suggested name escape the spill directory", async () => {
38
+ const { writes, resolver } = guestFileRecorder();
39
+ const store = createSpillStore(withSession(), resolver);
40
+ const ref = await store.saveText({
41
+ owner: { sessionId: SESSION_ID },
42
+ source: {},
43
+ suggestedName: "../../etc/passwd",
44
+ content: "x",
45
+ });
46
+ assert.ok(ref.locator.startsWith(`/tmp/dsh-podman/spill/${SESSION_ID}/`));
47
+ assert.ok(!ref.locator.includes(".."));
48
+ assert.equal(writes.length, 1);
49
+ });
50
+ test("saveText rejects when the session has no working directory", async () => {
51
+ const { resolver } = guestFileRecorder();
52
+ const store = createSpillStore(fakeCtx({}), resolver);
53
+ await assert.rejects(() => store.saveText({
54
+ owner: { sessionId: SESSION_ID },
55
+ source: {},
56
+ suggestedName: "a.txt",
57
+ content: "x",
58
+ }), /no session working directory/);
59
+ });
@@ -0,0 +1,9 @@
1
+ import { type WorkspaceResolver } from "./workspace-binding.js";
2
+ export interface SubprocessProvider {
3
+ resolveExecutable(command: string): Promise<string>;
4
+ spawn(spec: any): any;
5
+ spawnTerminal(spec: any): Promise<any>;
6
+ dispose(): void;
7
+ }
8
+ export declare function createSubprocessProvider(resolver: WorkspaceResolver): SubprocessProvider;
9
+ export { OutputReader, outputReader } from "./output-reader.js";