@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,412 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { approvalPath, currentCwd } from "./guest-rpc.js";
5
+ import { inferMountKind, projectMountDestinationReason } from "./mount-input.js";
6
+ import { defaultMountMode } from "./mount-enums.js";
7
+ import { TOOLS } from "./tool-schemas.js";
8
+ import { renderDenial, renderReason, } from "./approval-reasons.js";
9
+ // Build the mount fact for one mount object, or undefined when it names no
10
+ // source. `readOnly` is the mount's mode when the call carries one; omit it
11
+ // when the call does not (removal), so the reason names no mode.
12
+ function mountFact(mount, readOnly) {
13
+ const inferred = inferMountKind(mount);
14
+ const kind = inferred === "volume" || inferred === "secret" || inferred === "tmpfs"
15
+ ? inferred
16
+ : "project";
17
+ const destination = typeof mount.destination === "string" && mount.destination !== ""
18
+ ? mount.destination
19
+ : undefined;
20
+ const source = kind === "tmpfs"
21
+ ? ""
22
+ : kind === "volume"
23
+ ? typeof mount.volume === "string" ? mount.volume : ""
24
+ : kind === "secret"
25
+ ? typeof mount.secret === "string" ? mount.secret : ""
26
+ : typeof mount.project === "string"
27
+ ? mount.project
28
+ : "";
29
+ if (kind !== "tmpfs" && source === "")
30
+ return undefined;
31
+ return {
32
+ kind,
33
+ source,
34
+ ...(destination === undefined ? {} : { destination }),
35
+ ...(readOnly === undefined ? {} : { readOnly }),
36
+ };
37
+ }
38
+ // Build the locale-independent reason fact for a gated call, or undefined when
39
+ // the arguments carry too little to describe the call.
40
+ export function reasonFact(name, args, sessionCwd) {
41
+ const str = (key) => typeof args[key] === "string" && args[key] !== "" ? args[key] : undefined;
42
+ const listOf = (key) => {
43
+ const value = args[key];
44
+ return Array.isArray(value) && value.length > 0
45
+ ? value.map((item) => String(item))
46
+ : undefined;
47
+ };
48
+ const mountItems = () => {
49
+ const value = args.mounts;
50
+ if (!Array.isArray(value) || value.length === 0)
51
+ return undefined;
52
+ const items = value
53
+ .map((item) => {
54
+ if (typeof item !== "object" || item === null)
55
+ return undefined;
56
+ const mount = item;
57
+ const mode = typeof mount.mode === "string" && mount.mode !== ""
58
+ ? mount.mode
59
+ : defaultMountMode(inferMountKind(mount));
60
+ return mountFact(mount, mode !== "read_write");
61
+ })
62
+ .filter((item) => item !== undefined);
63
+ return items.length === 0 ? undefined : items;
64
+ };
65
+ const envKeys = () => {
66
+ const value = args.env;
67
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
68
+ return undefined;
69
+ }
70
+ const keys = Object.keys(value);
71
+ return keys.length === 0 ? undefined : keys;
72
+ };
73
+ const numberListOf = (key) => {
74
+ const value = args[key];
75
+ return Array.isArray(value) && value.length > 0
76
+ ? value.map((item) => Number(item))
77
+ : undefined;
78
+ };
79
+ // The optional process identity a tool may carry, shared by the tools that
80
+ // can run a process as another user.
81
+ const identityFact = () => {
82
+ const uid = typeof args.uid === "number" ? args.uid : undefined;
83
+ const gid = typeof args.gid === "number" ? args.gid : undefined;
84
+ const groups = numberListOf("groups");
85
+ return {
86
+ ...(uid === undefined ? {} : { uid }),
87
+ ...(gid === undefined ? {} : { gid }),
88
+ ...(groups === undefined ? {} : { groups }),
89
+ };
90
+ };
91
+ switch (name) {
92
+ case "image_build": {
93
+ const image = str("imageId");
94
+ if (image === undefined)
95
+ return undefined;
96
+ const parent = str("parent");
97
+ const packages = listOf("packages");
98
+ return {
99
+ kind: "image_build",
100
+ image,
101
+ ...(parent === undefined ? {} : { parent }),
102
+ ...(packages === undefined ? {} : { packages }),
103
+ };
104
+ }
105
+ case "image_rebuild": {
106
+ const image = str("imageId");
107
+ return image === undefined ? undefined : { kind: "image_rebuild", image };
108
+ }
109
+ case "image_rebuild_all":
110
+ return { kind: "image_rebuild_all" };
111
+ case "image_remove": {
112
+ const image = str("imageId");
113
+ return image === undefined ? undefined : { kind: "image_remove", image };
114
+ }
115
+ case "container_recreate":
116
+ case "container_start": {
117
+ const container = str("container");
118
+ if (container === undefined)
119
+ return undefined;
120
+ const image = str("image");
121
+ const mounts = mountItems();
122
+ const env = envKeys();
123
+ const paths = listOf("paths");
124
+ return {
125
+ kind: name,
126
+ container,
127
+ ...(image === undefined ? {} : { image }),
128
+ ...(mounts === undefined ? {} : { mounts }),
129
+ ...(env === undefined ? {} : { env }),
130
+ ...(paths === undefined ? {} : { paths }),
131
+ };
132
+ }
133
+ case "container_remove": {
134
+ const container = str("container");
135
+ return container === undefined ? undefined : { kind: "container_remove", container };
136
+ }
137
+ case "volume_remove": {
138
+ const volume = str("name");
139
+ return volume === undefined ? undefined : { kind: "volume_remove", name: volume };
140
+ }
141
+ case "secret_remove": {
142
+ const secret = str("name");
143
+ return secret === undefined ? undefined : { kind: "secret_remove", name: secret };
144
+ }
145
+ case "container_secret_add": {
146
+ const container = str("container");
147
+ const env = str("env");
148
+ const secret = str("secret");
149
+ if (container === undefined || env === undefined || secret === undefined) {
150
+ return undefined;
151
+ }
152
+ return { kind: "container_secret_add", container, secret, env };
153
+ }
154
+ case "container_secret_remove": {
155
+ const container = str("container");
156
+ const env = str("env");
157
+ if (container === undefined || env === undefined)
158
+ return undefined;
159
+ return { kind: "container_secret_remove", container, env };
160
+ }
161
+ case "container_mount_add":
162
+ case "container_mount_remove":
163
+ case "container_mount_update": {
164
+ const container = str("container");
165
+ if (container === undefined)
166
+ return undefined;
167
+ // Removal carries no mode, so its reason names none; add and update name
168
+ // the mode the call asks for (add defaults it like the mount schema).
169
+ const readOnly = name === "container_mount_remove"
170
+ ? undefined
171
+ : (typeof args.mode === "string" && args.mode !== ""
172
+ ? args.mode
173
+ : defaultMountMode(inferMountKind(args))) !== "read_write";
174
+ const mount = mountFact(args, readOnly);
175
+ if (mount === undefined)
176
+ return undefined;
177
+ return { kind: name, container, mount };
178
+ }
179
+ case "container_path_set": {
180
+ const container = str("container");
181
+ if (container === undefined)
182
+ return undefined;
183
+ const paths = Array.isArray(args.paths)
184
+ ? args.paths.map((path) => String(path))
185
+ : [];
186
+ return { kind: "container_path_set", container, paths };
187
+ }
188
+ case "container_path_add":
189
+ case "container_path_remove": {
190
+ const container = str("container");
191
+ const path = str("path");
192
+ if (container === undefined || path === undefined)
193
+ return undefined;
194
+ return { kind: name, container, path };
195
+ }
196
+ case "container_bash": {
197
+ const container = str("container");
198
+ const command = str("command");
199
+ if (container === undefined || command === undefined)
200
+ return undefined;
201
+ const cwd = str("workdir");
202
+ return {
203
+ kind: "container_bash",
204
+ container,
205
+ command,
206
+ ...(cwd === undefined ? {} : { cwd }),
207
+ ...identityFact(),
208
+ };
209
+ }
210
+ case "container_exec": {
211
+ const container = str("container");
212
+ const argv = listOf("argv");
213
+ if (container === undefined || argv === undefined)
214
+ return undefined;
215
+ const cwd = str("workdir");
216
+ return {
217
+ kind: "container_exec",
218
+ container,
219
+ argv,
220
+ ...(cwd === undefined ? {} : { cwd }),
221
+ ...identityFact(),
222
+ };
223
+ }
224
+ case "container_write":
225
+ case "container_edit": {
226
+ const container = str("container");
227
+ const path = str("file_path");
228
+ if (container === undefined || path === undefined)
229
+ return undefined;
230
+ return { kind: name, container, path: approvalPath(path, sessionCwd) };
231
+ }
232
+ case "daemon_start": {
233
+ const container = str("container");
234
+ const argv = listOf("argv");
235
+ if (container === undefined || argv === undefined)
236
+ return undefined;
237
+ const daemon = str("name");
238
+ return {
239
+ kind: "daemon_start",
240
+ container,
241
+ argv,
242
+ ...(daemon === undefined ? {} : { name: daemon }),
243
+ ...identityFact(),
244
+ };
245
+ }
246
+ default:
247
+ return undefined;
248
+ }
249
+ }
250
+ // Build the single-line approval summary shown for a gated tool call.
251
+ //
252
+ // sessionCwd is used to name the resolved target of a relative path, so the
253
+ // prompt describes the file that will actually be touched. `locale` selects
254
+ // the language; it defaults to English.
255
+ export function summarizeArgs(name, args, sessionCwd, locale = "en") {
256
+ const fact = reasonFact(name, args, sessionCwd);
257
+ return fact === undefined ? "" : renderReason(locale, fact);
258
+ }
259
+ // The ask reason, omitted when no summary can be derived so the approval panel
260
+ // shows its own localized fallback instead of a blank headline (the panel only
261
+ // substitutes for a nullish reason, not an empty string).
262
+ function askReason(name, args, sessionCwd, locale = "en") {
263
+ const reason = summarizeArgs(name, args, sessionCwd, locale);
264
+ return reason === "" ? {} : { reason };
265
+ }
266
+ // Decide whether a tool call needs approval. DSH resolves an `ask` decision
267
+ // through its approval service (`ctx.get("approval").request(...)`), showing
268
+ // the standard approval prompt; without one the call fails closed.
269
+ // Approval is per tool, optionally conditional on the call's arguments via a
270
+ // tool's `approvalWhen` predicate (e.g. container_start only asks when project
271
+ // mounts are supplied).
272
+ export function approvalDecision(name, args, sessionCwd, locale = "en") {
273
+ const tool = TOOLS.find((entry) => entry.name === name);
274
+ if (tool === undefined)
275
+ return undefined;
276
+ if (tool.approval === true) {
277
+ return { kind: "ask", ...askReason(name, args ?? {}, sessionCwd, locale) };
278
+ }
279
+ if (tool.approvalWhen !== undefined && tool.approvalWhen(args ?? {})) {
280
+ return { kind: "ask", ...askReason(name, args ?? {}, sessionCwd, locale) };
281
+ }
282
+ return undefined;
283
+ }
284
+ // Tools that only read or list and remain safe under a read-only permission.
285
+ export const READ_ONLY_TOOLS = new Set([
286
+ "image_list",
287
+ "image_get",
288
+ "container_list",
289
+ "container_read",
290
+ "container_glob",
291
+ "container_grep",
292
+ "container_mount_list",
293
+ "volume_list",
294
+ "secret_list",
295
+ "daemon_list",
296
+ "daemon_logs",
297
+ ]);
298
+ // The agent preset id this plugin ships ("Podman operator mode"). Tools in
299
+ // PODMAN_OPS_APPROVAL_TOOLS are gated behind approval only for agents composed
300
+ // from this preset, so it can "do other things" (run commands, edit container
301
+ // files, start daemons) once the user approves.
302
+ export const PODMAN_OPS_PRESET = "podman-ops";
303
+ // Tools the Podman operator-mode preset keeps available but approval-gated.
304
+ export const PODMAN_OPS_APPROVAL_TOOLS = new Set([
305
+ "container_bash",
306
+ "container_exec",
307
+ "container_write",
308
+ "container_edit",
309
+ "daemon_start",
310
+ ]);
311
+ // Every tool this plugin registers.
312
+ const OUR_TOOL_NAMES = new Set(TOOLS.map((tool) => tool.name));
313
+ // The harness's built-in tools that touch the container's filesystem or spawn
314
+ // processes in it. The harness fences these with its fs sandbox and its
315
+ // bash/pwsh sandbox executors, but this plugin replaces ctx.fs/ctx.subprocess
316
+ // and unwraps the landlock-run fence, so nothing else enforces read-only for
317
+ // them. Deny them here instead. `pwsh` exists only on Windows; the other
318
+ // built-ins that can reach the container (`subagent`, `workflow`, `ralph`)
319
+ // delegate to child agents that inherit the session's mode, so this policy
320
+ // already covers them.
321
+ const BUILTIN_FILE_TOOLS = new Set(["write", "edit", "bash", "pwsh"]);
322
+ // The `tools/pre-execute` policy, driven by the session's permission knobs:
323
+ // - read-only sandbox: the built-in shell/file tools and their container
324
+ // counterparts run only when the read-only shell gate allows them (every
325
+ // mount is read-only); every other mutating tool is denied with a reason.
326
+ // - approval policy "never" (Full access): run without asking.
327
+ // - otherwise (Workspace Write): ask for the approval-gated tools, including
328
+ // the tools the Podman operator-mode preset gates per preset.
329
+ export async function preExecutePolicy(exec, next, getProjectsRoot, getLocale, readSession, readOnlyShell) {
330
+ const name = exec.name;
331
+ const locale = getLocale?.() ?? "en";
332
+ const session = exec.agent?.session;
333
+ const facts = session === undefined ? {} : readSession?.(session) ?? {};
334
+ if (facts.mode === "read-only" && readOnlyShell !== undefined) {
335
+ const gated = await readOnlyShell(exec, locale);
336
+ // `allow` delegates so the rest of the pre-execute waterfall still runs.
337
+ if (gated?.kind === "allow")
338
+ return next();
339
+ if (gated !== undefined)
340
+ return gated;
341
+ }
342
+ if (facts.mode === "read-only" && BUILTIN_FILE_TOOLS.has(name)) {
343
+ return {
344
+ kind: "deny",
345
+ reason: renderDenial(locale, { kind: "read_only_builtin", tool: name }),
346
+ };
347
+ }
348
+ if (!OUR_TOOL_NAMES.has(name))
349
+ return next();
350
+ if (facts.mode === "read-only") {
351
+ if (READ_ONLY_TOOLS.has(name))
352
+ return next();
353
+ return {
354
+ kind: "deny",
355
+ reason: renderDenial(locale, { kind: "read_only", tool: name }),
356
+ };
357
+ }
358
+ const args = exec.arguments;
359
+ const parsed = typeof args === "object" && args !== null ? args : undefined;
360
+ const projectsRoot = getProjectsRoot?.();
361
+ const denyReason = mountDestinationsReason(name, parsed, projectsRoot, locale);
362
+ if (denyReason !== undefined) {
363
+ return { kind: "deny", reason: denyReason };
364
+ }
365
+ if (facts.policy === "never")
366
+ return next();
367
+ const sessionCwd = currentCwd(exec);
368
+ if (facts.preset === PODMAN_OPS_PRESET && PODMAN_OPS_APPROVAL_TOOLS.has(name)) {
369
+ return { kind: "ask", ...askReason(name, parsed ?? {}, sessionCwd, locale) };
370
+ }
371
+ return approvalDecision(name, parsed, sessionCwd, locale) ?? next();
372
+ }
373
+ // The reason to deny a mount-bearing tool call that puts a destination on a
374
+ // project (or unnamed) mount, or undefined when the call is allowed. Checks the
375
+ // single-item tools (container_mount_add/remove) and the mounts arrays of
376
+ // container_start/container_recreate.
377
+ export function mountDestinationsReason(name, args, projectsRoot, locale = "en") {
378
+ if (args === undefined)
379
+ return undefined;
380
+ if (name === "container_mount_add" || name === "container_mount_remove" || name === "container_mount_update") {
381
+ return projectMountDestinationReason(projectsRoot, args, locale);
382
+ }
383
+ if (name === "container_start" || name === "container_recreate") {
384
+ const mounts = args.mounts;
385
+ if (!Array.isArray(mounts))
386
+ return undefined;
387
+ for (const item of mounts) {
388
+ if (typeof item !== "object" || item === null)
389
+ continue;
390
+ const reason = projectMountDestinationReason(projectsRoot, item, locale);
391
+ if (reason !== undefined)
392
+ return reason;
393
+ }
394
+ }
395
+ return undefined;
396
+ }
397
+ // The harness's sandbox-escalation reason prefix. `approveEscalation` builds it
398
+ // as `escalate sandbox to <mode>: <justification>` in English regardless of the
399
+ // UI language (it is the asker's audit reason, not localized copy), and every
400
+ // escalating tool (bash, pwsh, the fs tools) goes through that one helper.
401
+ export const SANDBOX_ESCALATION_REASON_PREFIX = "escalate sandbox to ";
402
+ // Whether an approval request is the harness asking to widen the sandbox mode
403
+ // for one call. In this deployment the sandbox is bypassed inside the container
404
+ // (the plugin replaces ctx.subprocess/ctx.fs and unwraps the landlock-run
405
+ // fence), so the grant changes nothing and the prompt is noise. Matching the
406
+ // reason rather than the tool name keeps a real approval for the same tool
407
+ // from ever being claimed: if the wording changes, the prompt simply returns.
408
+ export function isSandboxEscalation(request) {
409
+ const reason = request?.reason;
410
+ return (typeof reason === "string" &&
411
+ reason.startsWith(SANDBOX_ESCALATION_REASON_PREFIX));
412
+ }
@@ -0,0 +1 @@
1
+ export {};