@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,546 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { bytesText, currentCwd, fsError, guestCwd, outputLines, resolveToolBinding, runExec, sessionWorkspaceSlug, sliceLines, unaryGuest, } from "./guest-rpc.js";
5
+ import { inferMountKind, mountsFromInput, projectMountDestinationReason } from "./mount-input.js";
6
+ import { publicContainer, publicDaemon, publicImage, publicMount } from "./public.js";
7
+ import { grpc } from "./grpc/runtime-client.js";
8
+ import { defaultMountMode, mountKindToProto, mountModeToProto } from "./mount-enums.js";
9
+ import { containerNotFound, } from "./workspace-binding.js";
10
+ // Cap on paths `container_glob` returns, matching the built-in glob tool's
11
+ // default result limit; `--sort=modified` makes the retained head the newest.
12
+ const GLOB_MAX_RESULTS = 100;
13
+ // VCS metadata directories ripgrep must never descend into for a discovery
14
+ // listing (`--no-ignore --hidden` would otherwise surface them).
15
+ const GLOB_VCS_EXCLUDES = [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"];
16
+ // failOnSearchError turns a ripgrep failure into a tool error: exit code 2 means
17
+ // ripgrep rejected the pattern or could not read the tree, and returning an
18
+ // empty result would present that as a successful search.
19
+ function failOnSearchError(tool, result) {
20
+ if (result.exitCode !== 2)
21
+ return;
22
+ const detail = result.stderr.trim();
23
+ throw new Error(detail === "" ? `${tool} search failed (ripgrep exit code 2)` : detail);
24
+ }
25
+ // The terminal environment the harness's shell executors impose on every
26
+ // command (`ENV_OVERRIDES` in dsh-bash-local), so a container command sees the
27
+ // same non-interactive terminal as the built-in `bash`.
28
+ const ENV_OVERRIDES = {
29
+ NO_COLOR: "1",
30
+ TERM: "dumb",
31
+ PAGER: "cat",
32
+ GIT_PAGER: "cat",
33
+ };
34
+ // managedEnv layers a container command's environment exactly like the
35
+ // harness's shell executors: the terminal overrides first, then the caller's
36
+ // entries, then the managed `DSH_*` snapshot last, so a caller cannot displace
37
+ // a harness-owned fact. Without the snapshot, `container_bash` would see a
38
+ // different command-visible environment than the built-in `bash`, which
39
+ // collects it through `ctx.shellEnv`.
40
+ function managedEnv(ctx, exec, env) {
41
+ const managed = ctx?.get?.("shellEnv")?.collect?.(exec) ?? {};
42
+ return { ...ENV_OVERRIDES, ...env, ...managed };
43
+ }
44
+ // containerTarget resolves a container-scoped file target through the plugin's
45
+ // own filesystem provider, so the container file tools share the harness's file
46
+ // contracts (the read-before-write guard, the binary rejection, and the same
47
+ // diagnostics) instead of calling the guest directly.
48
+ async function containerTarget(ctx, path, container, exec) {
49
+ return await ctx.fs.resolve(path, {
50
+ cwd: currentCwd(exec),
51
+ container: container ?? "",
52
+ signal: exec?.signal,
53
+ });
54
+ }
55
+ // identityFromInput validates the optional uid/gid/groups identity a tool
56
+ // accepts and returns it, or undefined when none was requested. Values are
57
+ // numeric only: the container's /etc/passwd is on the read-only rootfs, so
58
+ // there are no names to resolve.
59
+ function identityFromInput(input) {
60
+ const identity = {};
61
+ if (input.uid !== undefined) {
62
+ if (!Number.isInteger(input.uid) || input.uid < 0) {
63
+ throw new Error("uid must be an integer >= 0");
64
+ }
65
+ identity.uid = input.uid;
66
+ }
67
+ if (input.gid !== undefined) {
68
+ if (!Number.isInteger(input.gid) || input.gid < 0) {
69
+ throw new Error("gid must be an integer >= 0");
70
+ }
71
+ identity.gid = input.gid;
72
+ }
73
+ if (input.groups !== undefined) {
74
+ if (!Array.isArray(input.groups) ||
75
+ input.groups.some((group) => !Number.isInteger(group) || group < 0)) {
76
+ throw new Error("groups must be an array of integers >= 0");
77
+ }
78
+ identity.groups = input.groups;
79
+ }
80
+ return identity.uid === undefined &&
81
+ identity.gid === undefined &&
82
+ identity.groups === undefined
83
+ ? undefined
84
+ : identity;
85
+ }
86
+ export const toolHandlers = {
87
+ image_list: async (resolver) => {
88
+ const result = await resolver.control("listImages", {});
89
+ return (result.images ?? []).map(publicImage);
90
+ },
91
+ image_get: async (resolver, input) => publicImage(await resolver.control("getImage", { imageId: input.imageId })),
92
+ image_build: async (resolver, input) => publicImage(await resolver.control("buildImage", {
93
+ imageId: input.imageId,
94
+ parent: input.parent,
95
+ packages: input.packages,
96
+ })),
97
+ image_rebuild: async (resolver, input) => publicImage(await resolver.control("rebuildImage", { imageId: input.imageId })),
98
+ image_rebuild_all: async (resolver) => {
99
+ const result = await resolver.control("rebuildAllImages", {});
100
+ return {
101
+ rebuilt: result.rebuilt ?? [],
102
+ skipped: result.skipped ?? [],
103
+ };
104
+ },
105
+ image_remove: async (resolver, input) => {
106
+ await resolver.control("removeImage", { imageId: input.imageId });
107
+ return { removed: input.imageId };
108
+ },
109
+ container_list: async (resolver, _input, exec) => {
110
+ const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
111
+ const result = await resolver.control("listContainers", {});
112
+ const rows = (result.containers ?? []).filter((row) => row.workspaceSlug === slug);
113
+ const defaultRow = rows.find((row) => row.containerName === "default");
114
+ const listed = defaultRow === undefined
115
+ ? [{ containerName: "default", status: "not started" }]
116
+ : [publicContainer(defaultRow)];
117
+ return [
118
+ ...listed,
119
+ ...rows
120
+ .filter((row) => row.containerName !== "default")
121
+ .map(publicContainer),
122
+ ];
123
+ },
124
+ container_start: async (resolver, input, exec) => {
125
+ const mounts = mountsFromInput(input.mounts, resolver.getConfig().projectsRoot);
126
+ const row = await resolver.control("startContainer", {
127
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
128
+ container: input.container,
129
+ imageId: input.image,
130
+ ...(mounts === undefined ? {} : { mounts }),
131
+ ...(input.env !== undefined ? { env: input.env } : {}),
132
+ ...(input.secretEnv !== undefined ? { secretEnv: input.secretEnv } : {}),
133
+ ...(input.paths !== undefined ? { paths: input.paths } : {}),
134
+ });
135
+ return publicContainer(row);
136
+ },
137
+ container_recreate: async (resolver, input, exec) => {
138
+ const mounts = mountsFromInput(input.mounts, resolver.getConfig().projectsRoot);
139
+ const row = await resolver.control("recreateContainer", {
140
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
141
+ container: input.container,
142
+ imageId: input.image ?? "",
143
+ ...(mounts === undefined ? {} : { mounts }),
144
+ ...(input.env !== undefined ? { env: input.env } : {}),
145
+ ...(input.secretEnv !== undefined ? { secretEnv: input.secretEnv } : {}),
146
+ ...(input.paths !== undefined ? { paths: input.paths } : {}),
147
+ });
148
+ return publicContainer(row);
149
+ },
150
+ container_remove: async (resolver, input, exec) => {
151
+ await resolver.control("removeContainer", {
152
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
153
+ container: input.container,
154
+ });
155
+ return { removed: input.container };
156
+ },
157
+ container_bash: async (resolver, input, exec, ctx) => {
158
+ const sessionCwd = currentCwd(exec);
159
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
160
+ return runExec(binding, ["bash", "-c", input.command], guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input));
161
+ },
162
+ container_exec: async (resolver, input, exec, ctx) => {
163
+ const sessionCwd = currentCwd(exec);
164
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
165
+ return runExec(binding, input.argv, guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input));
166
+ },
167
+ container_read: async (resolver, input, exec, ctx) => {
168
+ const target = await containerTarget(ctx, input.file_path, input.container, exec);
169
+ // Mirror the harness's read tool: stat first so a miss is FS_NOT_FOUND and
170
+ // a directory is FS_NOT_REGULAR_FILE, then record the read. The observation
171
+ // is what satisfies the read-before-write guard for a later
172
+ // container_write/container_edit on the same path.
173
+ const info = await ctx.fs.stat(target, exec?.signal);
174
+ if (info === undefined) {
175
+ ctx.emit("fs/observed", target, { kind: "absent" }, exec);
176
+ throw fsError("FS_NOT_FOUND", `cannot read "${target.displayPath}": not found`);
177
+ }
178
+ if (info.type !== "file") {
179
+ throw fsError("FS_NOT_REGULAR_FILE", `cannot read "${target.displayPath}": not a regular file`);
180
+ }
181
+ const content = await ctx.fs.readText(target, exec?.signal);
182
+ ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
183
+ return sliceLines(content, input.offset, input.limit);
184
+ },
185
+ container_write: async (resolver, input, exec, ctx) => {
186
+ const target = await containerTarget(ctx, input.file_path, input.container, exec);
187
+ // The harness's write tool computes the intent through this waterfall and
188
+ // the provider enforces it, so the container tool shares the same
189
+ // read-before-write guard and diagnostics.
190
+ const intent = await ctx.waterfall("fs/write-intent", target, exec, () => undefined);
191
+ const content = String(input.content ?? "");
192
+ const outcome = await ctx.fs.writeText(target, content, intent, exec?.signal);
193
+ ctx.emit("fs/observed", target, { kind: "present", version: outcome.version }, exec);
194
+ return { bytesWritten: Buffer.byteLength(content, "utf8") };
195
+ },
196
+ container_edit: async (resolver, input, exec, ctx) => {
197
+ const target = await containerTarget(ctx, input.file_path, input.container, exec);
198
+ const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => undefined);
199
+ const outcome = await ctx.fs.editText(target, {
200
+ oldString: input.old_string,
201
+ newString: input.new_string,
202
+ replaceAll: input.replace_all === true,
203
+ }, intent, exec?.signal);
204
+ ctx.emit("fs/observed", target, { kind: "present", version: outcome.version }, exec);
205
+ return { before: outcome.before, after: outcome.after };
206
+ },
207
+ container_glob: async (resolver, input, exec) => {
208
+ const sessionCwd = currentCwd(exec);
209
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
210
+ const argv = [
211
+ "rg",
212
+ "--files",
213
+ `--glob=${input.pattern}`,
214
+ "--sort=modified",
215
+ "--no-ignore",
216
+ "--hidden",
217
+ ...GLOB_VCS_EXCLUDES.flatMap((name) => [
218
+ `--glob=!**/${name}`,
219
+ `--glob=!**/${name}/**`,
220
+ ]),
221
+ ];
222
+ const result = await runExec(binding, argv, guestCwd(input.path, sessionCwd, binding));
223
+ failOnSearchError("glob", result);
224
+ const files = outputLines(result.stdout);
225
+ const capped = files.length > GLOB_MAX_RESULTS;
226
+ return {
227
+ files: capped ? files.slice(0, GLOB_MAX_RESULTS) : files,
228
+ ...(capped
229
+ ? {
230
+ note: `showing ${GLOB_MAX_RESULTS} of ${files.length} files in modification-time order; narrow pattern or path to see more`,
231
+ }
232
+ : {}),
233
+ };
234
+ },
235
+ container_grep: async (resolver, input, exec) => {
236
+ const sessionCwd = currentCwd(exec);
237
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
238
+ const cwd = guestCwd(undefined, sessionCwd, binding);
239
+ // The search root: the requested path, else the container's default (the
240
+ // session workspace when it is mounted). Ripgrep reads stdin instead of the
241
+ // working directory when it is given no path, so it is always explicit.
242
+ const root = guestCwd(input.path, sessionCwd, binding) ?? cwd;
243
+ const argv = ["rg", "-n"];
244
+ if (typeof input.include === "string" && input.include !== "") {
245
+ argv.push("--glob", input.include);
246
+ }
247
+ argv.push(input.pattern);
248
+ if (root !== undefined)
249
+ argv.push(root);
250
+ const result = await runExec(binding, argv, cwd);
251
+ failOnSearchError("grep", result);
252
+ return {
253
+ matches: outputLines(result.stdout),
254
+ };
255
+ },
256
+ container_mount_list: async (resolver, input, exec) => {
257
+ const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
258
+ const result = await resolver.control("listContainers", {});
259
+ const row = (result.containers ?? []).find((candidate) => candidate.workspaceSlug === slug &&
260
+ candidate.containerName === input.container);
261
+ if (row === undefined) {
262
+ throw containerNotFound(input.container, slug);
263
+ }
264
+ const mounts = row.mounts ?? [];
265
+ return mounts.map(publicMount);
266
+ },
267
+ container_mount_add: async (resolver, input, exec) => {
268
+ const kind = inferMountKind(input);
269
+ const projectsRoot = resolver.getConfig().projectsRoot;
270
+ const destinationReason = projectMountDestinationReason(projectsRoot, input);
271
+ if (destinationReason !== undefined)
272
+ throw new Error(destinationReason);
273
+ if (kind === "secret" && input.mode === "read_write") {
274
+ throw new Error("secret mounts are read-only; omit mode or use read_only");
275
+ }
276
+ const protoKind = mountKindToProto(kind);
277
+ const mode = mountModeToProto(input.mode ?? defaultMountMode(kind));
278
+ const request = {
279
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
280
+ container: input.container,
281
+ kind: protoKind,
282
+ };
283
+ if (kind === "volume") {
284
+ request.volume = input.volume;
285
+ request.destination = input.destination;
286
+ request.mode = mode;
287
+ }
288
+ else if (kind === "tmpfs") {
289
+ request.destination = input.destination;
290
+ request.mode = mode;
291
+ }
292
+ else if (kind === "secret") {
293
+ request.secret = input.secret;
294
+ request.destination = input.destination;
295
+ }
296
+ else {
297
+ request.project = input.project;
298
+ request.mode = mode;
299
+ }
300
+ const row = await resolver.control("addContainerMount", request);
301
+ return publicContainer(row);
302
+ },
303
+ container_mount_remove: async (resolver, input, exec) => {
304
+ const kind = inferMountKind(input);
305
+ const protoKind = mountKindToProto(kind);
306
+ const request = {
307
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
308
+ container: input.container,
309
+ kind: protoKind,
310
+ };
311
+ if (kind === "project") {
312
+ request.project = input.project;
313
+ }
314
+ else if (kind === "volume") {
315
+ if (input.volume !== undefined)
316
+ request.volume = input.volume;
317
+ }
318
+ else if (kind === "secret") {
319
+ if (input.secret !== undefined)
320
+ request.secret = input.secret;
321
+ }
322
+ if (kind !== "project" && input.destination !== undefined) {
323
+ request.destination = input.destination;
324
+ }
325
+ const row = await resolver.control("removeContainerMount", request);
326
+ return publicContainer(row);
327
+ },
328
+ container_mount_update: async (resolver, input, exec) => {
329
+ const kind = inferMountKind(input);
330
+ if (kind !== "project" && kind !== "volume") {
331
+ throw new Error(`only project and volume mounts carry a mode; ${kind} mounts cannot be remounted`);
332
+ }
333
+ const projectsRoot = resolver.getConfig().projectsRoot;
334
+ const destinationReason = projectMountDestinationReason(projectsRoot, input);
335
+ if (destinationReason !== undefined)
336
+ throw new Error(destinationReason);
337
+ const request = {
338
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
339
+ container: input.container,
340
+ kind: mountKindToProto(kind),
341
+ mode: mountModeToProto(input.mode),
342
+ };
343
+ if (kind === "project") {
344
+ request.project = input.project;
345
+ }
346
+ else {
347
+ if (input.volume !== undefined)
348
+ request.volume = input.volume;
349
+ if (input.destination !== undefined) {
350
+ request.destination = input.destination;
351
+ }
352
+ }
353
+ const row = await resolver.control("updateContainerMount", request);
354
+ return publicContainer(row);
355
+ },
356
+ container_path_set: async (resolver, input, exec) => {
357
+ const sessionCwd = currentCwd(exec);
358
+ const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
359
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
360
+ const paths = Array.isArray(input.paths) ? input.paths.map(String) : [];
361
+ return {
362
+ paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
363
+ };
364
+ },
365
+ container_path_add: async (resolver, input, exec) => {
366
+ const sessionCwd = currentCwd(exec);
367
+ const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
368
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
369
+ const current = await containerPathState(binding);
370
+ // Prepending gives the new path the highest priority; an existing entry is
371
+ // moved to the front rather than duplicated.
372
+ const paths = [
373
+ input.path,
374
+ ...current.paths.filter((path) => path !== input.path),
375
+ ];
376
+ return {
377
+ paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
378
+ };
379
+ },
380
+ container_path_remove: async (resolver, input, exec) => {
381
+ const sessionCwd = currentCwd(exec);
382
+ const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
383
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
384
+ const current = await containerPathState(binding);
385
+ if (!current.paths.includes(input.path)) {
386
+ if (current.defaultPaths.includes(input.path)) {
387
+ throw new Error(`path ${JSON.stringify(input.path)} is part of the container's default PATH and cannot be removed`);
388
+ }
389
+ throw new Error(`path ${JSON.stringify(input.path)} is not an added path`);
390
+ }
391
+ const paths = current.paths.filter((path) => path !== input.path);
392
+ return {
393
+ paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
394
+ };
395
+ },
396
+ volume_list: async (resolver) => {
397
+ const result = await resolver.control("listVolumes", {});
398
+ return (result.volumes ?? []).map((volume) => ({ name: volume.name }));
399
+ },
400
+ volume_create: async (resolver, input) => {
401
+ await resolver.control("createVolume", { name: input.name });
402
+ return { name: input.name };
403
+ },
404
+ volume_remove: async (resolver, input) => {
405
+ await resolver.control("removeVolume", { name: input.name });
406
+ return { removed: input.name };
407
+ },
408
+ secret_list: async (resolver) => {
409
+ const result = await resolver.control("listSecrets", {});
410
+ return (result.secrets ?? []).map((secret) => ({ name: secret.name }));
411
+ },
412
+ secret_create: async (resolver, input) => {
413
+ await resolver.control("createSecret", {
414
+ name: input.name,
415
+ ...(input.length ? { length: input.length } : {}),
416
+ ...(input.charset ? { charset: input.charset } : {}),
417
+ });
418
+ return { name: input.name };
419
+ },
420
+ secret_remove: async (resolver, input) => {
421
+ await resolver.control("removeSecret", { name: input.name });
422
+ return { removed: input.name };
423
+ },
424
+ container_secret_add: async (resolver, input, exec) => {
425
+ const row = await resolver.control("addContainerSecret", {
426
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
427
+ container: input.container,
428
+ env: input.env,
429
+ secret: input.secret,
430
+ });
431
+ return publicContainer(row);
432
+ },
433
+ container_secret_remove: async (resolver, input, exec) => {
434
+ const row = await resolver.control("removeContainerSecret", {
435
+ workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
436
+ container: input.container,
437
+ env: input.env,
438
+ });
439
+ return publicContainer(row);
440
+ },
441
+ daemon_start: async (resolver, input, exec) => {
442
+ const sessionCwd = currentCwd(exec);
443
+ const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
444
+ const request = {
445
+ argv: input.argv,
446
+ inheritEnv: { value: input.inheritEnv !== false },
447
+ };
448
+ if (input.name !== undefined)
449
+ request.name = input.name;
450
+ const cwd = guestCwd(input.cwd, sessionCwd, binding);
451
+ if (cwd !== undefined)
452
+ request.cwd = cwd;
453
+ if (input.env !== undefined)
454
+ request.env = input.env;
455
+ const identity = identityFromInput(input);
456
+ if (identity?.uid !== undefined)
457
+ request.uid = { value: identity.uid };
458
+ if (identity?.gid !== undefined)
459
+ request.gid = { value: identity.gid };
460
+ if (identity?.groups !== undefined)
461
+ request.groups = identity.groups;
462
+ const info = await unaryGuest({ binding }, "startDaemon", request);
463
+ return publicDaemon(info);
464
+ },
465
+ daemon_list: async (resolver, input, exec) => {
466
+ const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
467
+ const result = (await unaryGuest({ binding }, "listDaemons", {}));
468
+ return (result.daemons ?? []).map(publicDaemon);
469
+ },
470
+ daemon_stop: async (resolver, input, exec) => {
471
+ const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
472
+ await daemonCall(input.name, () => unaryGuest({ binding }, "stopDaemon", {
473
+ name: input.name,
474
+ signal: input.signal,
475
+ }));
476
+ return { stopped: input.name };
477
+ },
478
+ daemon_restart: async (resolver, input, exec) => {
479
+ const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
480
+ const info = await daemonCall(input.name, () => unaryGuest({ binding }, "restartDaemon", { name: input.name }));
481
+ return publicDaemon(info);
482
+ },
483
+ daemon_logs: async (resolver, input, exec) => {
484
+ const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
485
+ const result = (await daemonCall(input.name, () => unaryGuest({ binding }, "daemonLogs", {
486
+ name: input.name,
487
+ tailBytes: input.tailBytes,
488
+ })));
489
+ return {
490
+ stdout: bytesText(result.stdout),
491
+ stderr: bytesText(result.stderr),
492
+ };
493
+ },
494
+ };
495
+ function daemonNotFound(name) {
496
+ const error = new Error(`daemon ${JSON.stringify(name)} not found`);
497
+ error.code = "NOT_FOUND";
498
+ return error;
499
+ }
500
+ // containerPathState reads the guest's live PATH additions and the default PATH
501
+ // they are prepended to.
502
+ async function containerPathState(binding) {
503
+ const result = (await unaryGuest({ binding }, "getPaths", {}));
504
+ const paths = Array.isArray(result?.paths)
505
+ ? result.paths.map((path) => String(path))
506
+ : [];
507
+ const defaultPath = typeof result?.defaultPath === "string" ? result.defaultPath : "";
508
+ return {
509
+ paths,
510
+ defaultPaths: defaultPath === "" ? [] : defaultPath.split(":"),
511
+ };
512
+ }
513
+ // applyContainerPaths persists the list on the container and pushes it to the
514
+ // running guest agent, returning the applied (validated, deduplicated) list.
515
+ // The guest holds the live list; the container record is what a recreate
516
+ // restores it from.
517
+ async function applyContainerPaths(resolver, binding, slug, container, paths) {
518
+ const row = (await resolver.control("setContainerPaths", {
519
+ workspaceSlug: slug,
520
+ container,
521
+ paths: [...paths],
522
+ }));
523
+ const persisted = Array.isArray(row?.paths)
524
+ ? row.paths.map((path) => String(path))
525
+ : [...paths];
526
+ const result = (await unaryGuest({ binding }, "setPaths", {
527
+ paths: persisted,
528
+ }));
529
+ return Array.isArray(result?.paths)
530
+ ? result.paths.map((path) => String(path))
531
+ : persisted;
532
+ }
533
+ // daemonCall maps the guest agent's generic "unknown daemon" to a message that
534
+ // names the daemon the caller asked about.
535
+ async function daemonCall(name, call) {
536
+ try {
537
+ return await call();
538
+ }
539
+ catch (error) {
540
+ if (error.code === grpc.status.NOT_FOUND &&
541
+ /unknown daemon/.test(error.message)) {
542
+ throw daemonNotFound(name);
543
+ }
544
+ throw error;
545
+ }
546
+ }