@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,437 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { EventEmitter } from "node:events";
5
+ import { createFilesystemProvider } from "./index.js";
6
+ // WORKSPACE_ID is a valid workspace id: slugs are UUIDs, and the pod/container
7
+ // names are derived from one.
8
+ export const WORKSPACE_ID = "2c573001-4171-4900-904b-12a5cc02737a";
9
+ export class FakeTerminalCall extends EventEmitter {
10
+ stdinChunks = [];
11
+ write(message) {
12
+ if (message.start) {
13
+ queueMicrotask(() => this.emit("data", { started: { pid: 42 } }));
14
+ return;
15
+ }
16
+ if (message.stdinChunk) {
17
+ this.stdinChunks.push(Buffer.from(message.stdinChunk));
18
+ return;
19
+ }
20
+ if (message.inspectRequestId !== undefined) {
21
+ const requestId = message.inspectRequestId;
22
+ queueMicrotask(() => this.emit("data", {
23
+ foreground: { requestId, found: true, processGroupId: 7, inputWaiting: true },
24
+ }));
25
+ return;
26
+ }
27
+ if (message.signal) {
28
+ const requestId = message.signal.requestId;
29
+ queueMicrotask(() => this.emit("data", {
30
+ signalled: { requestId, found: true, processGroupId: 7 },
31
+ }));
32
+ }
33
+ }
34
+ end() { }
35
+ cancel() { }
36
+ emitStdout(data) {
37
+ this.emit("data", { stdoutChunk: data });
38
+ }
39
+ emitExit(exitCode = 0, signaled = false) {
40
+ this.emit("data", { exit: { exitCode, signaled, signal: "" } });
41
+ this.emit("end");
42
+ }
43
+ }
44
+ export const fakeTerminalResolver = (fake) => ({
45
+ resolveForPath: async () => ({
46
+ guest: { terminal: () => fake },
47
+ token: "t",
48
+ }),
49
+ });
50
+ export const stubResolver = {
51
+ resolve: async () => ({ kind: "binding" }),
52
+ resolveForPath: async () => ({ kind: "binding" }),
53
+ };
54
+ export const EXPECTED_TOOLS = [
55
+ "image_list",
56
+ "image_get",
57
+ "image_build",
58
+ "image_rebuild",
59
+ "image_rebuild_all",
60
+ "image_remove",
61
+ "container_list",
62
+ "container_start",
63
+ "container_recreate",
64
+ "container_remove",
65
+ "container_bash",
66
+ "container_exec",
67
+ "container_read",
68
+ "container_write",
69
+ "container_edit",
70
+ "container_glob",
71
+ "container_grep",
72
+ "container_mount_list",
73
+ "container_mount_add",
74
+ "container_mount_remove",
75
+ "container_mount_update",
76
+ "container_path_set",
77
+ "container_path_add",
78
+ "container_path_remove",
79
+ "volume_list",
80
+ "volume_create",
81
+ "volume_remove",
82
+ "secret_list",
83
+ "secret_create",
84
+ "secret_remove",
85
+ "container_secret_add",
86
+ "container_secret_remove",
87
+ "daemon_start",
88
+ "daemon_list",
89
+ "daemon_stop",
90
+ "daemon_restart",
91
+ "daemon_logs",
92
+ ];
93
+ export const testReadSession = (session) => session?.facts ?? {};
94
+ export const sessionExec = (name, facts = { mode: "read-only" }) => ({
95
+ name,
96
+ agent: { session: { facts } },
97
+ });
98
+ export const presetExec = (name, preset, args, facts = {}) => ({
99
+ name,
100
+ arguments: args,
101
+ agent: { session: { header: { agentPreset: preset }, facts: { ...facts, preset } } },
102
+ });
103
+ export const MOUNT_TOOLS = [
104
+ "container_mount_list",
105
+ "container_mount_add",
106
+ "container_mount_remove",
107
+ "container_mount_update",
108
+ ];
109
+ export const VOLUME_TOOLS = ["volume_list", "volume_create", "volume_remove"];
110
+ export const SECRET_TOOLS = [
111
+ "secret_list",
112
+ "secret_create",
113
+ "secret_remove",
114
+ "container_secret_add",
115
+ "container_secret_remove",
116
+ ];
117
+ export function mountRequestRecorder() {
118
+ const requests = [];
119
+ const resolver = {
120
+ registry: {
121
+ resolveByPath: async () => ({ id: WORKSPACE_ID }),
122
+ },
123
+ getConfig: () => ({ projectsRoot: "/projects" }),
124
+ async control(method, request) {
125
+ requests.push([method, request]);
126
+ return {};
127
+ },
128
+ };
129
+ return { requests, resolver };
130
+ }
131
+ export const MOUNT_EXEC = { agent: { session: { header: { cwd: "/proj" } } } };
132
+ export const DAEMON_TOOLS = [
133
+ "daemon_start",
134
+ "daemon_list",
135
+ "daemon_stop",
136
+ "daemon_restart",
137
+ "daemon_logs",
138
+ ];
139
+ export const SECRET_BEARING_CONTAINER = {
140
+ workspaceSlug: WORKSPACE_ID,
141
+ containerName: "default",
142
+ imageId: "img-1",
143
+ status: "running",
144
+ createdAt: "2026-01-01T00:00:00Z",
145
+ podmanName: `dsh-podman-${WORKSPACE_ID}-default`,
146
+ agentSocketPath: `/run/dsh-podman/${WORKSPACE_ID}-default/guest.sock`,
147
+ agentToken: "super-secret-token",
148
+ mounts: [
149
+ { projectName: "team", mode: "MOUNT_MODE_READ_WRITE", kind: "MOUNT_KIND_PROJECT" },
150
+ ],
151
+ env: { PATH: "/bin", DB_PASSWORD: "hunter2" },
152
+ secretEnv: { DB_PASS: "db-pass" },
153
+ };
154
+ export function secretBearingResolver() {
155
+ return {
156
+ registry: {
157
+ resolveByPath: async () => ({ id: WORKSPACE_ID }),
158
+ },
159
+ getConfig: () => ({ projectsRoot: "/projects" }),
160
+ control: async () => SECRET_BEARING_CONTAINER,
161
+ };
162
+ }
163
+ // Stubs the guest agent's streaming ReadFile/WriteFile calls, recording the
164
+ // paths the tools ask for.
165
+ export function guestFileRecorder(content = "hello", exists = true) {
166
+ const reads = [];
167
+ const writes = [];
168
+ const mkdirs = [];
169
+ const guest = {
170
+ readFile: (request) => {
171
+ reads.push(request.path);
172
+ return {
173
+ async *[Symbol.asyncIterator]() {
174
+ yield { data: Buffer.from(content) };
175
+ },
176
+ cancel() { },
177
+ };
178
+ },
179
+ mkdir: (request, _metadata, callback) => {
180
+ mkdirs.push(request.path);
181
+ callback(null, {});
182
+ return { cancel() { } };
183
+ },
184
+ stat: (_request, _metadata, callback) => {
185
+ callback(null, exists
186
+ ? {
187
+ exists: true,
188
+ isDir: false,
189
+ size: Buffer.byteLength(content),
190
+ modifiedAt: "1",
191
+ mode: 0o644,
192
+ }
193
+ : { exists: false });
194
+ return { cancel() { } };
195
+ },
196
+ writeFile: (_metadata, _options, callback) => {
197
+ let path = "";
198
+ let body = "";
199
+ return {
200
+ write(message) {
201
+ if (message.start !== undefined)
202
+ path = message.start.path;
203
+ if (message.dataChunk !== undefined)
204
+ body += String(message.dataChunk);
205
+ },
206
+ end() {
207
+ writes.push({ path, content: body });
208
+ callback(null, { bytesWritten: body.length });
209
+ },
210
+ };
211
+ },
212
+ };
213
+ const binding = { guest, token: "t", socket: "/run/x.sock" };
214
+ const resolver = {
215
+ resolve: async () => binding,
216
+ containerBinding: async () => binding,
217
+ resolveForPath: async () => binding,
218
+ };
219
+ return { reads, writes, mkdirs, resolver };
220
+ }
221
+ // fakeToolContext is the minimal cordis context the container file tools need:
222
+ // the plugin's real filesystem provider over the given resolver, the fs
223
+ // observation waterfalls stubbed to "no intent" (an unconditional write), and
224
+ // an `emit` that records the `fs/observed` events the tools report.
225
+ export function fakeToolContext(resolver) {
226
+ const observed = [];
227
+ return {
228
+ fs: createFilesystemProvider(resolver),
229
+ observed,
230
+ async waterfall(_name, _target, _actor, next) {
231
+ return await next();
232
+ },
233
+ emit(event, target, observation) {
234
+ if (event === "fs/observed")
235
+ observed.push({ target, observation });
236
+ },
237
+ };
238
+ }
239
+ // Stubs the guest agent's streaming Exec call, recording the start message.
240
+ export function guestExecRecorder(defaultCwd, stdout) {
241
+ const starts = [];
242
+ const guest = {
243
+ exec: () => {
244
+ const handlers = {};
245
+ return {
246
+ on(event, handler) {
247
+ (handlers[event] ??= []).push(handler);
248
+ },
249
+ write(message) {
250
+ starts.push({
251
+ argv: message.start.argv,
252
+ cwd: message.start.cwd,
253
+ env: message.start.env,
254
+ ...(message.start.uid !== undefined
255
+ ? { uid: message.start.uid.value }
256
+ : {}),
257
+ ...(message.start.gid !== undefined
258
+ ? { gid: message.start.gid.value }
259
+ : {}),
260
+ ...(message.start.groups !== undefined
261
+ ? { groups: message.start.groups }
262
+ : {}),
263
+ });
264
+ },
265
+ end() {
266
+ for (const handler of handlers.data ?? []) {
267
+ if (stdout !== undefined)
268
+ handler({ stdoutChunk: Buffer.from(stdout) });
269
+ handler({ exit: { exitCode: 0, signaled: false } });
270
+ }
271
+ },
272
+ };
273
+ },
274
+ };
275
+ const binding = {
276
+ guest,
277
+ token: "t",
278
+ socket: "/run/x.sock",
279
+ ...(defaultCwd === undefined ? {} : { defaultCwd }),
280
+ };
281
+ const resolver = {
282
+ resolve: async () => binding,
283
+ containerBinding: async () => binding,
284
+ };
285
+ return { starts, resolver };
286
+ }
287
+ // A resolver for the container_path_* tools: the guest stubs GetPaths/SetPaths
288
+ // (the agent's live list) and control records the setContainerPaths call.
289
+ export function pathRequestRecorder(options = {}) {
290
+ const controlCalls = [];
291
+ const setCalls = [];
292
+ const defaultPath = options.defaultPath ?? "/usr/bin";
293
+ let paths = options.paths ?? [];
294
+ const guest = {
295
+ getPaths: (_request, _metadata, callback) => callback(null, { paths, defaultPath }),
296
+ setPaths: (request, _metadata, callback) => {
297
+ setCalls.push(request.paths);
298
+ paths = request.paths;
299
+ callback(null, { paths, defaultPath });
300
+ },
301
+ };
302
+ const binding = { guest, token: "t", socket: "/run/x.sock" };
303
+ const resolver = {
304
+ registry: { resolveByPath: async () => ({ id: WORKSPACE_ID }) },
305
+ getConfig: () => ({ projectsRoot: "/projects" }),
306
+ resolve: async () => binding,
307
+ containerBinding: async () => binding,
308
+ async control(method, request) {
309
+ controlCalls.push([method, request]);
310
+ return { paths: request.paths ?? [] };
311
+ },
312
+ };
313
+ return { controlCalls, setCalls, resolver };
314
+ }
315
+ // A fake guest whose readFile honors offset/length, so the provider's byte
316
+ // windowing and text decoding can be exercised without a container.
317
+ export function fakeGuest(source) {
318
+ const requests = [];
319
+ return {
320
+ requests,
321
+ guest: {
322
+ readFile: (request) => {
323
+ requests.push(request);
324
+ return {
325
+ async *[Symbol.asyncIterator]() {
326
+ const offset = Number(request.offset ?? 0);
327
+ const length = Number(request.length ?? 0);
328
+ const slice = length > 0
329
+ ? source.subarray(offset, offset + length)
330
+ : source.subarray(offset);
331
+ yield { data: slice };
332
+ },
333
+ };
334
+ },
335
+ },
336
+ };
337
+ }
338
+ export function providerFor(guest) {
339
+ return createFilesystemProvider({
340
+ resolveForPath: async () => ({ guest, token: "t" }),
341
+ });
342
+ }
343
+ // A fake guest for the text/byte provider methods: a stat result, a one-chunk
344
+ // readFile, and a no-op writeFile.
345
+ export function editGuest(stat, content = Buffer.alloc(0)) {
346
+ const writes = [];
347
+ return {
348
+ writes,
349
+ guest: {
350
+ stat: (_request, _metadata, callback) => callback(null, {
351
+ exists: true,
352
+ isDir: false,
353
+ isSymlink: false,
354
+ size: String(content.length),
355
+ mode: "-rw-r--r--",
356
+ modifiedAt: "t",
357
+ ...stat,
358
+ }),
359
+ readFile: () => ({
360
+ async *[Symbol.asyncIterator]() {
361
+ yield { data: content };
362
+ },
363
+ }),
364
+ writeFile: (_metadata, _options, callback) => {
365
+ const call = {
366
+ write(message) {
367
+ if (message?.dataChunk)
368
+ writes.push(Buffer.from(message.dataChunk));
369
+ },
370
+ end() { },
371
+ };
372
+ callback(null, { bytesWritten: 0 });
373
+ return call;
374
+ },
375
+ },
376
+ token: "t",
377
+ };
378
+ }
379
+ export function editProvider(binding) {
380
+ return createFilesystemProvider({
381
+ resolveForPath: async () => binding,
382
+ });
383
+ }
384
+ // A fake guest for the subprocess provider: exec emits a process id (and
385
+ // optionally an exit), and the unary signal/delete RPCs are recorded.
386
+ export function spawnGuest(options = {}) {
387
+ const signals = [];
388
+ const starts = [];
389
+ const deletes = [];
390
+ const guest = {
391
+ exec: () => {
392
+ const handlers = {};
393
+ return {
394
+ on(event, handler) {
395
+ (handlers[event] ??= []).push(handler);
396
+ },
397
+ write(message) {
398
+ starts.push(message);
399
+ },
400
+ end() {
401
+ for (const handler of handlers.data ?? []) {
402
+ handler({ processId: "7" });
403
+ if (options.stdout !== undefined) {
404
+ handler({ stdoutChunk: Buffer.from(options.stdout) });
405
+ }
406
+ if (options.emitExit !== false) {
407
+ handler({
408
+ exit: { exitCode: 0, signaled: false, ...options.exit },
409
+ });
410
+ }
411
+ }
412
+ },
413
+ };
414
+ },
415
+ signal: (request, _metadata, callback) => {
416
+ signals.push(request);
417
+ callback(null, {});
418
+ },
419
+ delete: (request, _metadata, callback) => {
420
+ deletes.push(request);
421
+ callback(null, {});
422
+ },
423
+ };
424
+ return {
425
+ signals,
426
+ starts,
427
+ deletes,
428
+ resolver: { resolve: async () => ({ guest, token: "t" }) },
429
+ };
430
+ }
431
+ export const spawnSpec = (overrides = {}) => ({
432
+ argv: ["sleep", "1"],
433
+ cwd: "/projects/team",
434
+ stdio: { stdin: "ignore", stdout: { maxBytes: 10 }, stderr: { maxBytes: 10 } },
435
+ graceMs: 50,
436
+ ...overrides,
437
+ });
@@ -0,0 +1,26 @@
1
+ export declare function defineTool<T>(definition: T): T;
2
+ export declare const toolOutput: {
3
+ schema: {
4
+ type: string;
5
+ };
6
+ render: (_args: unknown, value: string) => {
7
+ type: string;
8
+ text: string;
9
+ }[];
10
+ };
11
+ export interface ToolDefinition {
12
+ name: string;
13
+ parameters: {
14
+ type: string;
15
+ properties: Record<string, any>;
16
+ required: readonly string[];
17
+ };
18
+ approval?: boolean;
19
+ approvalWhen?: (args: Record<string, unknown>) => boolean;
20
+ }
21
+ export declare const TOOLS: ToolDefinition[];
22
+ export declare const TOOL_DESCRIPTIONS: Record<string, string>;
23
+ export declare const TOOL_UI: Record<string, {
24
+ title: string;
25
+ kind: string;
26
+ }>;
@@ -0,0 +1,188 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { containerBashParameters, containerEditParameters, containerExecParameters, containerGlobParameters, containerGrepParameters, containerListParameters, containerMountAddParameters, containerMountListParameters, containerMountRemoveParameters, containerMountUpdateParameters, containerPathAddParameters, containerPathRemoveParameters, containerPathSetParameters, containerReadParameters, containerRecreateParameters, containerRemoveParameters, containerSecretAddParameters, containerSecretRemoveParameters, containerStartParameters, containerWriteParameters, daemonListParameters, daemonLogsParameters, daemonRestartParameters, daemonStartParameters, daemonStopParameters, imageBuildParameters, imageGetParameters, imageListParameters, imageRebuildAllParameters, imageRebuildParameters, imageRemoveParameters, secretCreateParameters, secretListParameters, secretRemoveParameters, volumeCreateParameters, volumeListParameters, volumeRemoveParameters } from "./tool-params.js";
5
+ export function defineTool(definition) {
6
+ return definition;
7
+ }
8
+ export const toolOutput = {
9
+ schema: { type: "string" },
10
+ render: (_args, value) => [{ type: "text", text: value }],
11
+ };
12
+ export const TOOLS = [
13
+ { name: "image_list", parameters: imageListParameters },
14
+ { name: "image_get", parameters: imageGetParameters },
15
+ {
16
+ name: "image_build",
17
+ parameters: imageBuildParameters,
18
+ approval: true,
19
+ },
20
+ { name: "image_rebuild", parameters: imageRebuildParameters, approval: true },
21
+ {
22
+ name: "image_rebuild_all",
23
+ parameters: imageRebuildAllParameters,
24
+ approval: true,
25
+ },
26
+ { name: "image_remove", parameters: imageRemoveParameters, approval: true },
27
+ { name: "container_list", parameters: containerListParameters },
28
+ {
29
+ name: "container_start",
30
+ parameters: containerStartParameters,
31
+ approvalWhen: (args) => Array.isArray(args.mounts) && args.mounts.length > 0,
32
+ },
33
+ {
34
+ name: "container_recreate",
35
+ parameters: containerRecreateParameters,
36
+ approval: true,
37
+ },
38
+ { name: "container_remove", parameters: containerRemoveParameters, approval: true },
39
+ { name: "container_bash", parameters: containerBashParameters },
40
+ { name: "container_exec", parameters: containerExecParameters },
41
+ { name: "container_read", parameters: containerReadParameters },
42
+ { name: "container_write", parameters: containerWriteParameters },
43
+ { name: "container_edit", parameters: containerEditParameters },
44
+ { name: "container_glob", parameters: containerGlobParameters },
45
+ { name: "container_grep", parameters: containerGrepParameters },
46
+ {
47
+ name: "container_mount_list",
48
+ parameters: containerMountListParameters,
49
+ },
50
+ {
51
+ name: "container_mount_add",
52
+ parameters: containerMountAddParameters,
53
+ approval: true,
54
+ },
55
+ {
56
+ name: "container_mount_remove",
57
+ parameters: containerMountRemoveParameters,
58
+ approval: true,
59
+ },
60
+ {
61
+ name: "container_mount_update",
62
+ parameters: containerMountUpdateParameters,
63
+ approval: true,
64
+ },
65
+ {
66
+ name: "container_path_set",
67
+ parameters: containerPathSetParameters,
68
+ approval: true,
69
+ },
70
+ {
71
+ name: "container_path_add",
72
+ parameters: containerPathAddParameters,
73
+ approval: true,
74
+ },
75
+ {
76
+ name: "container_path_remove",
77
+ parameters: containerPathRemoveParameters,
78
+ approval: true,
79
+ },
80
+ { name: "volume_list", parameters: volumeListParameters },
81
+ { name: "volume_create", parameters: volumeCreateParameters },
82
+ { name: "volume_remove", parameters: volumeRemoveParameters, approval: true },
83
+ { name: "secret_list", parameters: secretListParameters },
84
+ {
85
+ name: "secret_create",
86
+ parameters: secretCreateParameters,
87
+ },
88
+ {
89
+ name: "secret_remove",
90
+ parameters: secretRemoveParameters,
91
+ approval: true,
92
+ },
93
+ {
94
+ name: "container_secret_add",
95
+ parameters: containerSecretAddParameters,
96
+ approval: true,
97
+ },
98
+ {
99
+ name: "container_secret_remove",
100
+ parameters: containerSecretRemoveParameters,
101
+ approval: true,
102
+ },
103
+ { name: "daemon_start", parameters: daemonStartParameters },
104
+ { name: "daemon_list", parameters: daemonListParameters },
105
+ { name: "daemon_stop", parameters: daemonStopParameters },
106
+ { name: "daemon_restart", parameters: daemonRestartParameters },
107
+ { name: "daemon_logs", parameters: daemonLogsParameters },
108
+ ];
109
+ export const TOOL_DESCRIPTIONS = {
110
+ image_list: "List the images of the current workspace by short name, base images first, then custom images. Base images are managed from the settings.",
111
+ image_get: "Get details about a specific image by its short name.",
112
+ image_build: "Build a new custom image from a parent image by short name (a base like archlinux/ubuntu/alpine, or an existing custom image) and a set of packages. Base images themselves are managed from the settings. Requires approval: building installs packages system-wide into a container image.",
113
+ image_rebuild: "Rebuild an existing custom image by short name. Base images are rebuilt from the settings. Requires approval: rebuilding replaces the current image content.",
114
+ image_rebuild_all: "Rebuild every stored image in dependency order (base first), skipping any image whose rebuild fails and its dependents. Requires approval: rebuilding replaces the images' contents.",
115
+ image_remove: "Remove a built custom image by short name. Base images are managed from the settings. Requires approval: removing deletes the image so containers using it must be recreated from another image.",
116
+ container_list: "List the containers of the current workspace, including the default container that is started on demand.",
117
+ container_start: "Start a container in the current workspace. Named containers are created with exactly the mounts you pass — the project directory is not mounted automatically. The default container always keeps its workspace project mount. Pass paths to replace the container's PATH additions.",
118
+ container_recreate: "Recreate a container in the current workspace, keeping its current image when no image is given, optionally with new project mounts. Pass paths to replace the container's PATH additions. Requires approval: recreating replaces the running container.",
119
+ container_remove: "Remove a container from the current workspace.",
120
+ container_bash: "Run a shell command inside a container of the current workspace. Defaults to the session working directory when it is mounted; pass workdir to override.",
121
+ container_exec: "Run a program inside a container of the current workspace. Defaults to the session working directory when it is mounted; pass cwd to override.",
122
+ container_read: "Read a file inside a container of the current workspace.",
123
+ container_write: "Write a file inside a container of the current workspace.",
124
+ container_edit: "Edit a file inside a container of the current workspace.",
125
+ container_glob: "List files inside a container of the current workspace matching a glob pattern. A pattern with no \"/\" matches basenames at any depth. Results are files only, include hidden and ignored files, and exclude VCS metadata directories. Defaults to the session working directory when it is mounted; pass path to override.",
126
+ container_grep: "Search file contents inside a container of the current workspace. Defaults to the session working directory when it is mounted; pass cwd to override.",
127
+ container_mount_list: "List the mounts of a container in the current workspace, with the exact kind, project, volume, secret, and destination values to pass to container_mount_remove.",
128
+ container_mount_add: "Add a mount to a container in the current workspace: a project bind (project, a path under the projects root), a tmpfs, a named volume, or a secret, selected by kind (default project). Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: adding a mount changes the container filesystem view.",
129
+ container_mount_remove: "Remove a mount from a container in the current workspace. Identify it by kind plus its own handle: project for a project mount, volume for a named volume, secret for a secret, and destination for tmpfs. A handle that matches more than one mount is rejected, so pass destination as well when a volume or secret is mounted more than once; copy the exact values from container_mount_list. Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: removing a mount changes the container filesystem view.",
130
+ container_mount_update: "Change the mode of an existing project or volume mount in the current workspace (read_only or read_write). Identify it by kind plus its own handle, exactly like container_mount_remove; a handle that matches more than one mount is rejected. The default container's workspace project mount can be remounted read-only. Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: changing a mount's mode changes the container filesystem view.",
131
+ container_path_set: "Replace the directories the container prepends to PATH for every command it runs, highest priority first. Applied immediately to future shell, exec, terminal, and daemon runs without recreating the container; already-running daemons keep their old PATH. A recreated container restores the list. Requires approval: changing PATH changes which binaries run.",
132
+ container_path_add: "Prepend one directory to the container's PATH additions, moving it to the front when it is already there. Applied immediately without recreating the container; already-running daemons keep their old PATH. Requires approval: changing PATH changes which binaries run.",
133
+ container_path_remove: "Remove one directory from the container's PATH additions. A directory that is only part of the container's default PATH cannot be removed. Applied immediately without recreating the container; already-running daemons keep their old PATH. Requires approval: changing PATH changes which binaries run.",
134
+ volume_list: "List the named volumes available to the current workspace.",
135
+ volume_create: "Create a named volume in the current workspace.",
136
+ volume_remove: "Remove a named volume from the current workspace. Refused while a container still mounts it; detach the mount first.",
137
+ secret_list: "List the named secrets available to the current workspace (values are never exposed).",
138
+ secret_create: "Create a secret with an orchestrator-generated random value (the value is never exposed).",
139
+ secret_remove: "Remove a named secret from the current workspace. Refused while a container mounts it or attaches it as an environment variable; detach it first. Requires approval: removing deletes the secret.",
140
+ container_secret_add: "Inject a named secret into a container as an environment variable. Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: exposing a secret to a container changes what it can read.",
141
+ container_secret_remove: "Stop injecting a named secret into a container environment variable. Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: removing a secret exposure changes what the container can read.",
142
+ daemon_start: "Start a daemon inside a container of the current workspace. Optionally run it as a specific uid/gid (with optional supplementary groups). The daemon inherits the container's environment by default; pass inheritEnv=false to give it only PATH, HOME, and env. Defaults to the session working directory when it is mounted; pass cwd to override. An existing daemon with the same name is stopped and replaced.",
143
+ daemon_list: "List the daemons running inside a container of the current workspace. Daemons live in the container's guest agent and do not survive a container recreate.",
144
+ daemon_stop: "Stop a daemon inside a container of the current workspace.",
145
+ daemon_restart: "Restart a daemon inside a container of the current workspace.",
146
+ daemon_logs: "Read the captured logs of a daemon inside a container of the current workspace.",
147
+ };
148
+ // UI title and icon category per tool, used by the host presenters so a UI can
149
+ // label a call meaningfully instead of showing the raw tool name.
150
+ export const TOOL_UI = {
151
+ image_list: { title: "List images", kind: "search" },
152
+ image_get: { title: "Inspect image", kind: "read" },
153
+ image_build: { title: "Build image", kind: "execute" },
154
+ image_rebuild: { title: "Rebuild image", kind: "execute" },
155
+ image_rebuild_all: { title: "Rebuild all images", kind: "execute" },
156
+ image_remove: { title: "Remove image", kind: "delete" },
157
+ container_list: { title: "List containers", kind: "search" },
158
+ container_start: { title: "Start container", kind: "execute" },
159
+ container_recreate: { title: "Recreate container", kind: "execute" },
160
+ container_remove: { title: "Remove container", kind: "delete" },
161
+ container_bash: { title: "Container bash", kind: "execute" },
162
+ container_exec: { title: "Container exec", kind: "execute" },
163
+ container_read: { title: "Container read", kind: "read" },
164
+ container_write: { title: "Container write", kind: "edit" },
165
+ container_edit: { title: "Container edit", kind: "edit" },
166
+ container_glob: { title: "Container glob", kind: "search" },
167
+ container_grep: { title: "Container grep", kind: "search" },
168
+ container_mount_list: { title: "List mounts", kind: "read" },
169
+ container_mount_add: { title: "Add mount", kind: "edit" },
170
+ container_mount_remove: { title: "Remove mount", kind: "delete" },
171
+ container_mount_update: { title: "Change mount mode", kind: "edit" },
172
+ container_path_set: { title: "Set PATH additions", kind: "edit" },
173
+ container_path_add: { title: "Add PATH entry", kind: "edit" },
174
+ container_path_remove: { title: "Remove PATH entry", kind: "delete" },
175
+ volume_list: { title: "List volumes", kind: "search" },
176
+ volume_create: { title: "Create volume", kind: "execute" },
177
+ volume_remove: { title: "Remove volume", kind: "delete" },
178
+ secret_list: { title: "List secrets", kind: "search" },
179
+ secret_create: { title: "Create secret", kind: "execute" },
180
+ secret_remove: { title: "Remove secret", kind: "delete" },
181
+ container_secret_add: { title: "Attach secret", kind: "edit" },
182
+ container_secret_remove: { title: "Detach secret", kind: "delete" },
183
+ daemon_start: { title: "Start daemon", kind: "execute" },
184
+ daemon_list: { title: "List daemons", kind: "read" },
185
+ daemon_stop: { title: "Stop daemon", kind: "delete" },
186
+ daemon_restart: { title: "Restart daemon", kind: "execute" },
187
+ daemon_logs: { title: "Daemon logs", kind: "read" },
188
+ };
@@ -0,0 +1,2 @@
1
+ import { WorkspaceResolver } from "./workspace-binding.js";
2
+ export declare const toolHandlers: Record<string, (resolver: WorkspaceResolver, input: any, exec: any, ctx?: any) => Promise<unknown>>;