@exagone313/dsh-podman 0.2.0-rc.3 → 0.2.0

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 (191) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +14 -2
  3. package/cordis.patch.yml +58 -0
  4. package/dist/approval-reasons.d.ts +3 -4
  5. package/dist/approval-reasons.js +40 -49
  6. package/dist/approval-reasons.test.js +43 -15
  7. package/dist/approval.js +27 -10
  8. package/dist/approval.test.js +165 -34
  9. package/dist/card-route.d.ts +3 -2
  10. package/dist/card-route.js +154 -42
  11. package/dist/card-route.test.js +85 -43
  12. package/dist/card-test-support.js +39 -66
  13. package/dist/client/ContainerCard.d.ts +1 -1
  14. package/dist/client/ContainerCard.js +92 -50
  15. package/dist/client/card-protocol.d.ts +6 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +16 -18
  18. package/dist/client/container-card-controller.js +92 -40
  19. package/dist/client/container-card-create-modal.d.ts +2 -2
  20. package/dist/client/container-card-create-modal.js +13 -5
  21. package/dist/client/container-card-default-env.d.ts +10 -0
  22. package/dist/client/container-card-default-env.js +69 -0
  23. package/dist/client/container-card-directory.d.ts +2 -2
  24. package/dist/client/container-card-directory.js +12 -4
  25. package/dist/client/container-card-editors.d.ts +3 -3
  26. package/dist/client/container-card-editors.js +104 -32
  27. package/dist/client/container-card-images.d.ts +4 -4
  28. package/dist/client/container-card-images.js +19 -6
  29. package/dist/client/container-card-paths.d.ts +3 -3
  30. package/dist/client/container-card-paths.js +18 -11
  31. package/dist/client/container-card-row.d.ts +10 -6
  32. package/dist/client/container-card-row.js +82 -13
  33. package/dist/client/container-card-secrets.d.ts +3 -3
  34. package/dist/client/container-card-secrets.js +8 -6
  35. package/dist/client/container-card-shared.d.ts +4 -17
  36. package/dist/client/container-card-shared.js +9 -25
  37. package/dist/client/container-card-styles.d.ts +2 -8
  38. package/dist/client/container-card-styles.js +14 -52
  39. package/dist/client/container-card-volumes.d.ts +2 -2
  40. package/dist/client/container-card-volumes.js +9 -5
  41. package/dist/client/container-card-workspace.d.ts +8 -6
  42. package/dist/client/container-card-workspace.js +9 -3
  43. package/dist/client/directory-picker.js +2 -1
  44. package/dist/client/index.js +13141 -2036
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +106 -38
  47. package/dist/client/podman-terminal-guide.d.ts +11 -0
  48. package/dist/client/podman-terminal-guide.js +208 -0
  49. package/dist/client/podman-terminal-title.d.ts +7 -0
  50. package/dist/client/podman-terminal-title.js +30 -0
  51. package/dist/client/podman-terminal.d.ts +26 -0
  52. package/dist/client/podman-terminal.js +412 -0
  53. package/dist/client/read-only-approval.d.ts +2 -2
  54. package/dist/client/read-only-approval.js +13 -3
  55. package/dist/client/slot-contract.d.ts +0 -11
  56. package/dist/client/terminal-preference.d.ts +26 -0
  57. package/dist/client/terminal-preference.js +67 -0
  58. package/dist/client/terminal-protocol.d.ts +85 -0
  59. package/dist/client/terminal-protocol.js +14 -0
  60. package/dist/client/terminal-tab.d.ts +60 -0
  61. package/dist/client/terminal-tab.js +35 -0
  62. package/dist/client/terminal-targets.d.ts +33 -0
  63. package/dist/client/terminal-targets.js +65 -0
  64. package/dist/client/terminal-titles.d.ts +23 -0
  65. package/dist/client/terminal-titles.js +52 -0
  66. package/dist/client/terminal-transport.d.ts +63 -0
  67. package/dist/client/terminal-transport.js +207 -0
  68. package/dist/client/tool-views.js +215 -49
  69. package/dist/container-env.d.ts +11 -0
  70. package/dist/container-env.js +96 -0
  71. package/dist/container-env.test.d.ts +1 -0
  72. package/dist/container-env.test.js +93 -0
  73. package/dist/containers.test.js +196 -18
  74. package/dist/daemons.test.js +18 -4
  75. package/dist/dsh-version.d.ts +1 -0
  76. package/dist/dsh-version.js +46 -0
  77. package/dist/env-rows.d.ts +14 -0
  78. package/dist/env-rows.js +52 -0
  79. package/dist/env-rows.test.d.ts +1 -0
  80. package/dist/env-rows.test.js +43 -0
  81. package/dist/fs-provider.d.ts +1 -0
  82. package/dist/fs-provider.js +26 -12
  83. package/dist/fs-provider.test.js +51 -10
  84. package/dist/generated/version.d.ts +2 -2
  85. package/dist/generated/version.js +2 -2
  86. package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
  87. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  88. package/dist/grpc/runtime-client.d.ts +3 -1
  89. package/dist/grpc/runtime-client.js +90 -17
  90. package/dist/guest-rpc.d.ts +15 -19
  91. package/dist/guest-rpc.js +289 -70
  92. package/dist/guest-rpc.test.d.ts +1 -0
  93. package/dist/guest-rpc.test.js +241 -0
  94. package/dist/guest-terminal.d.ts +30 -0
  95. package/dist/guest-terminal.js +196 -0
  96. package/dist/images.test.js +10 -3
  97. package/dist/index.d.ts +11 -16
  98. package/dist/index.js +42 -36
  99. package/dist/locales.test.d.ts +1 -0
  100. package/dist/locales.test.js +34 -0
  101. package/dist/misc.test.js +38 -10
  102. package/dist/mount-enums.js +2 -1
  103. package/dist/mount-input.d.ts +2 -0
  104. package/dist/mount-input.js +38 -22
  105. package/dist/mounts.test.js +81 -11
  106. package/dist/naming.test.d.ts +1 -0
  107. package/dist/naming.test.js +28 -0
  108. package/dist/output-reader.js +36 -4
  109. package/dist/package-deps.test.d.ts +1 -0
  110. package/dist/package-deps.test.js +49 -0
  111. package/dist/paths.test.js +4 -2
  112. package/dist/plugin-meta.test.d.ts +1 -0
  113. package/dist/plugin-meta.test.js +29 -0
  114. package/dist/project-path.js +33 -7
  115. package/dist/project-path.test.js +14 -0
  116. package/dist/prompts.d.ts +0 -4
  117. package/dist/prompts.js +15 -84
  118. package/dist/read-only-shell.js +3 -1
  119. package/dist/read-only-shell.test.js +80 -25
  120. package/dist/runtime-client.test.d.ts +1 -0
  121. package/dist/runtime-client.test.js +53 -0
  122. package/dist/secrets.test.js +21 -6
  123. package/dist/settings-commands.test.js +61 -14
  124. package/dist/settings-create.test.js +32 -10
  125. package/dist/settings-default-env.test.d.ts +1 -0
  126. package/dist/settings-default-env.test.js +175 -0
  127. package/dist/settings-mounts.test.js +46 -7
  128. package/dist/settings-schema.d.ts +6 -11
  129. package/dist/settings-schema.js +4 -8
  130. package/dist/settings-schema.test.d.ts +1 -0
  131. package/dist/settings-schema.test.js +35 -0
  132. package/dist/settings-workspaces.test.js +38 -63
  133. package/dist/spill-store.d.ts +1 -0
  134. package/dist/spill-store.js +17 -4
  135. package/dist/spill-store.test.js +31 -8
  136. package/dist/subprocess.d.ts +4 -0
  137. package/dist/subprocess.js +148 -162
  138. package/dist/subprocess.test.js +118 -8
  139. package/dist/terminal-preference.test.d.ts +1 -0
  140. package/dist/terminal-preference.test.js +62 -0
  141. package/dist/terminal-route.d.ts +10 -0
  142. package/dist/terminal-route.js +270 -0
  143. package/dist/terminal-route.test.d.ts +1 -0
  144. package/dist/terminal-route.test.js +265 -0
  145. package/dist/terminal-sessions.d.ts +69 -0
  146. package/dist/terminal-sessions.js +252 -0
  147. package/dist/terminal-shells.d.ts +22 -0
  148. package/dist/terminal-shells.js +99 -0
  149. package/dist/terminal-tab.test.d.ts +1 -0
  150. package/dist/terminal-tab.test.js +58 -0
  151. package/dist/terminal-targets.test.d.ts +1 -0
  152. package/dist/terminal-targets.test.js +43 -0
  153. package/dist/terminal-titles.test.d.ts +1 -0
  154. package/dist/terminal-titles.test.js +36 -0
  155. package/dist/test-support.d.ts +35 -5
  156. package/dist/test-support.js +91 -29
  157. package/dist/tool-defs.js +19 -4
  158. package/dist/tool-handlers.js +80 -28
  159. package/dist/tool-params.js +19 -5
  160. package/dist/version-compat.d.ts +4 -0
  161. package/dist/version-compat.js +29 -0
  162. package/dist/version-compat.test.d.ts +1 -0
  163. package/dist/version-compat.test.js +27 -0
  164. package/dist/volumes.test.js +1 -1
  165. package/dist/workspace-binding.d.ts +19 -10
  166. package/dist/workspace-binding.js +252 -78
  167. package/dist/workspace-binding.test.js +202 -27
  168. package/docs/architecture.md +71 -20
  169. package/docs/architecture.zh.md +62 -12
  170. package/docs/configuration.md +61 -20
  171. package/docs/configuration.zh.md +53 -19
  172. package/docs/development-prompt.md +72 -0
  173. package/docs/development-prompt.zh.md +69 -0
  174. package/docs/development.md +184 -29
  175. package/docs/development.zh.md +165 -26
  176. package/docs/install-dsh-and-dsh-podman.md +24 -21
  177. package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
  178. package/docs/uninstall.md +83 -0
  179. package/docs/uninstall.zh.md +79 -0
  180. package/docs/update.md +120 -0
  181. package/docs/update.zh.md +114 -0
  182. package/docs/usage.md +108 -61
  183. package/docs/usage.zh.md +76 -38
  184. package/locale/en.json +6 -0
  185. package/locale/zh.json +6 -0
  186. package/package.json +58 -25
  187. package/quadlet/dsh-podman-orchestrator.container +46 -0
  188. package/quadlet/dsh.container +35 -0
  189. package/LICENSE.pkg +0 -17103
  190. package/dist/preferences.d.ts +0 -6
  191. package/dist/preferences.js +0 -27
@@ -3,11 +3,13 @@
3
3
  // SPDX-License-Identifier: MIT
4
4
  import test from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
6
+ import { readFileSync } from "node:fs";
9
7
  import { presetExec, sessionExec, testReadSession } from "./test-support.js";
10
- import { PODMAN_OPS_AGENT_CORDIS_YML, PODMAN_OPS_PRESET_YML, READ_ONLY_TOOLS, SANDBOX_ESCALATION_REASON_PREFIX, TOOLS, approvalDecision, ensurePodmanOpsPreset, isSandboxEscalation, preExecutePolicy, summarizeArgs, } from "./index.js";
8
+ import { approvalDecision, isSandboxEscalation, preExecutePolicy, READ_ONLY_TOOLS, SANDBOX_ESCALATION_REASON_PREFIX, summarizeArgs, TOOLS, } from "./index.js";
9
+ // The bundle patch that declares the Podman operator preset.
10
+ function podmanOpsPatch() {
11
+ return readFileSync(new URL("../cordis.patch.yml", import.meta.url), "utf8");
12
+ }
11
13
  test("isSandboxEscalation claims only the harness's sandbox escalation", () => {
12
14
  assert.equal(SANDBOX_ESCALATION_REASON_PREFIX, "escalate sandbox to ");
13
15
  assert.equal(isSandboxEscalation({
@@ -130,7 +132,19 @@ test("summarizeArgs renders the approval reason for each gated tool", () => {
130
132
  assert.equal(summarizeArgs("image_build", {
131
133
  imageId: "dev",
132
134
  parent: "archlinux",
133
- packages: ["git", "curl", "tmux", "vim", "zsh", "openssh", "jq", "ripgrep", "make", "cc", "go"],
135
+ packages: [
136
+ "git",
137
+ "curl",
138
+ "tmux",
139
+ "vim",
140
+ "zsh",
141
+ "openssh",
142
+ "jq",
143
+ "ripgrep",
144
+ "make",
145
+ "cc",
146
+ "go",
147
+ ],
134
148
  }), 'Build image "dev" from "archlinux" with packages: git, curl, tmux, vim, zsh, openssh, jq, ripgrep, +3 more.');
135
149
  assert.equal(summarizeArgs("image_rebuild", { imageId: "valkey" }), 'Rebuild image "valkey".');
136
150
  assert.equal(summarizeArgs("image_rebuild_all", {}), "Rebuild all images.");
@@ -247,7 +261,12 @@ test("read-only permission allows get/list tools and denies the rest", async ()
247
261
  assert.equal(delegated, true, `${name} must be allowed under read-only`);
248
262
  assert.equal(result.kind, "allow");
249
263
  }
250
- for (const name of ["image_build", "container_start", "container_remove", "volume_remove"]) {
264
+ for (const name of [
265
+ "image_build",
266
+ "container_start",
267
+ "container_remove",
268
+ "volume_remove",
269
+ ]) {
251
270
  const result = (await preExecutePolicy(sessionExec(name), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
252
271
  assert.equal(result.kind, "deny", `${name} must be denied under read-only`);
253
272
  assert.ok(result.reason.includes("read-only"), `${name} deny reason`);
@@ -270,7 +289,9 @@ test("read-only denies the built-in file and shell tools", async () => {
270
289
  }
271
290
  });
272
291
  test("workspace-write and full access delegate the built-in file and shell tools", async () => {
273
- for (const facts of [{ mode: "workspace-write", policy: "ask" }, { policy: "never" }]) {
292
+ for (const facts of [{ mode: "workspace-write", policy: "ask" }, {
293
+ policy: "never",
294
+ }]) {
274
295
  for (const name of ["write", "edit", "bash", "pwsh"]) {
275
296
  let delegated = false;
276
297
  const result = (await preExecutePolicy(sessionExec(name, facts), () => {
@@ -322,10 +343,18 @@ test("read-only wins over a never approval policy", async () => {
322
343
  });
323
344
  test("podman-ops preset asks for its approval-gated tools only", async () => {
324
345
  const workspaceWrite = { mode: "workspace-write", policy: "ask" };
325
- const asked = (await preExecutePolicy(presetExec("container_bash", "podman-ops", { container: "valkey-ctr", command: "valkey-cli ping" }, workspaceWrite), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
346
+ const asked = (await preExecutePolicy(presetExec("container_bash", "podman-ops", {
347
+ container: "valkey-ctr",
348
+ command: "valkey-cli ping",
349
+ }, workspaceWrite), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
326
350
  assert.equal(asked.kind, "ask");
327
351
  assert.equal(asked.reason, 'Run a shell command in container "valkey-ctr": valkey-cli ping');
328
- const daemon = (await preExecutePolicy(presetExec("daemon_start", "podman-ops", { container: "valkey-ctr", name: "v1", argv: ["valkey-server"], uid: 1001 }, workspaceWrite), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
352
+ const daemon = (await preExecutePolicy(presetExec("daemon_start", "podman-ops", {
353
+ container: "valkey-ctr",
354
+ name: "v1",
355
+ argv: ["valkey-server"],
356
+ uid: 1001,
357
+ }, workspaceWrite), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
329
358
  assert.equal(daemon.kind, "ask");
330
359
  assert.equal(daemon.reason, 'Start daemon "v1" in container "valkey-ctr": valkey-server (uid 1001)');
331
360
  });
@@ -350,10 +379,17 @@ test("podman-ops keeps open tools ungated and respects permissions", async () =>
350
379
  }, undefined, undefined, testReadSession));
351
380
  assert.equal(delegated, true, "image_list must stay open in podman-ops");
352
381
  assert.equal(open.kind, "allow");
353
- const denied = (await preExecutePolicy(presetExec("container_bash", "podman-ops", { container: "c", command: "ls" }, { mode: "read-only" }), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
382
+ const denied = (await preExecutePolicy(presetExec("container_bash", "podman-ops", {
383
+ container: "c",
384
+ command: "ls",
385
+ }, {
386
+ mode: "read-only",
387
+ }), () => Promise.resolve({ kind: "allow" }), undefined, undefined, testReadSession));
354
388
  assert.equal(denied.kind, "deny", "read-only wins over podman-ops approval");
355
389
  let fullDelegated = false;
356
- const full = (await preExecutePolicy(presetExec("daemon_start", "podman-ops", { container: "c", argv: ["x"] }, { policy: "never" }), () => {
390
+ const full = (await preExecutePolicy(presetExec("daemon_start", "podman-ops", { container: "c", argv: ["x"] }, {
391
+ policy: "never",
392
+ }), () => {
357
393
  fullDelegated = true;
358
394
  return Promise.resolve({ kind: "allow" });
359
395
  }, undefined, undefined, testReadSession));
@@ -361,12 +397,45 @@ test("podman-ops keeps open tools ungated and respects permissions", async () =>
361
397
  assert.equal(full.kind, "allow");
362
398
  });
363
399
  test("summarizeArgs renders reasons for the podman-ops gated tools", () => {
364
- assert.equal(summarizeArgs("container_exec", { container: "c", argv: ["python", "run.py", "--x", "1", "--y", "2", "--z", "3", "--w", "4", "--v", "5"] }), 'Run a command in container "c": python run.py --x 1 --y 2 --z 3 …');
365
- assert.equal(summarizeArgs("container_write", { container: "c", file_path: "/etc/valkey/valkey.conf" }), 'Write "/etc/valkey/valkey.conf" in container "c".');
366
- assert.equal(summarizeArgs("container_edit", { container: "c", file_path: "/etc/valkey/valkey.conf" }), 'Edit "/etc/valkey/valkey.conf" in container "c".');
367
- assert.equal(summarizeArgs("container_bash", { container: "c", command: "ping -c 1 8.8.8.8" }), 'Run a shell command in container "c": ping -c 1 8.8.8.8');
368
- assert.equal(summarizeArgs("container_bash", { container: "c", command: "ls", workdir: "/srv" }), 'Run a shell command in container "c" (cwd "/srv"): ls');
369
- assert.equal(summarizeArgs("container_exec", { container: "c", argv: ["ls"], workdir: "/srv" }), 'Run a command in container "c" (cwd "/srv"): ls');
400
+ assert.equal(summarizeArgs("container_exec", {
401
+ container: "c",
402
+ argv: [
403
+ "python",
404
+ "run.py",
405
+ "--x",
406
+ "1",
407
+ "--y",
408
+ "2",
409
+ "--z",
410
+ "3",
411
+ "--w",
412
+ "4",
413
+ "--v",
414
+ "5",
415
+ ],
416
+ }), 'Run a command in container "c": python run.py --x 1 --y 2 --z 3 …');
417
+ assert.equal(summarizeArgs("container_write", {
418
+ container: "c",
419
+ file_path: "/etc/valkey/valkey.conf",
420
+ }), 'Write "/etc/valkey/valkey.conf" in container "c".');
421
+ assert.equal(summarizeArgs("container_edit", {
422
+ container: "c",
423
+ file_path: "/etc/valkey/valkey.conf",
424
+ }), 'Edit "/etc/valkey/valkey.conf" in container "c".');
425
+ assert.equal(summarizeArgs("container_bash", {
426
+ container: "c",
427
+ command: "ping -c 1 8.8.8.8",
428
+ }), 'Run a shell command in container "c": ping -c 1 8.8.8.8');
429
+ assert.equal(summarizeArgs("container_bash", {
430
+ container: "c",
431
+ command: "ls",
432
+ workdir: "/srv",
433
+ }), 'Run a shell command in container "c" (cwd "/srv"): ls');
434
+ assert.equal(summarizeArgs("container_exec", {
435
+ container: "c",
436
+ argv: ["ls"],
437
+ workdir: "/srv",
438
+ }), 'Run a command in container "c" (cwd "/srv"): ls');
370
439
  });
371
440
  test("summarizeArgs names the process identity and groups", () => {
372
441
  assert.equal(summarizeArgs("container_bash", {
@@ -376,7 +445,11 @@ test("summarizeArgs names the process identity and groups", () => {
376
445
  gid: 1000,
377
446
  groups: [3000, 4000],
378
447
  }), 'Run a shell command in container "c": id (uid 1000, gid 1000, groups 3000, 4000)');
379
- assert.equal(summarizeArgs("container_exec", { container: "c", argv: ["id"], gid: 2000 }), 'Run a command in container "c": id (gid 2000)');
448
+ assert.equal(summarizeArgs("container_exec", {
449
+ container: "c",
450
+ argv: ["id"],
451
+ gid: 2000,
452
+ }), 'Run a command in container "c": id (gid 2000)');
380
453
  assert.equal(summarizeArgs("daemon_start", {
381
454
  container: "c",
382
455
  name: "web",
@@ -385,9 +458,14 @@ test("summarizeArgs names the process identity and groups", () => {
385
458
  groups: [3000],
386
459
  }), 'Start daemon "web" in container "c": nginx (uid 1000, groups 3000)');
387
460
  // An empty groups list is not worth naming.
388
- assert.equal(summarizeArgs("container_exec", { container: "c", argv: ["id"], groups: [] }), 'Run a command in container "c": id');
461
+ assert.equal(summarizeArgs("container_exec", {
462
+ container: "c",
463
+ argv: ["id"],
464
+ groups: [],
465
+ }), 'Run a command in container "c": id');
389
466
  });
390
467
  test("Podman-ops preset content covers the recent tools", () => {
468
+ const patch = podmanOpsPatch();
391
469
  for (const tool of [
392
470
  "image_rebuild_all",
393
471
  "secret_list",
@@ -396,24 +474,26 @@ test("Podman-ops preset content covers the recent tools", () => {
396
474
  "container_secret_add",
397
475
  "container_secret_remove",
398
476
  ]) {
399
- assert.ok(PODMAN_OPS_AGENT_CORDIS_YML.includes(tool), `podman-ops composition must mention ${tool}`);
477
+ assert.ok(patch.includes(tool), `podman-ops composition must mention ${tool}`);
400
478
  }
401
- assert.ok(PODMAN_OPS_PRESET_YML.includes("secrets"), "podman-ops metadata must mention secrets");
479
+ assert.match(patch, /id: podman-ops/, "the preset row must carry the policy's preset id");
480
+ assert.ok(patch.includes("secrets"), "podman-ops metadata must mention secrets");
402
481
  // The persona plugin takes its prose as `prefix` (a required field); `text`
403
482
  // is not part of its schema and would make the preset fail to load.
404
- assert.match(PODMAN_OPS_AGENT_CORDIS_YML, /prefix:/);
405
- assert.doesNotMatch(PODMAN_OPS_AGENT_CORDIS_YML, /\btext:/);
483
+ assert.match(patch, /prefix:/);
484
+ assert.doesNotMatch(patch, /\btext:/);
406
485
  });
407
- test("Podman-ops preset writer overwrites existing content", () => {
408
- const dir = mkdtempSync(join(tmpdir(), "dsh-podman-"));
409
- ensurePodmanOpsPreset(undefined, dir);
410
- const composition = join(dir, "agent.cordis.yml");
411
- const metadata = join(dir, "preset.yml");
412
- assert.equal(readFileSync(composition, "utf8"), PODMAN_OPS_AGENT_CORDIS_YML);
413
- assert.equal(readFileSync(metadata, "utf8"), PODMAN_OPS_PRESET_YML);
414
- writeFileSync(composition, "# stale user copy\n");
415
- ensurePodmanOpsPreset(undefined, dir);
416
- assert.equal(readFileSync(composition, "utf8"), PODMAN_OPS_AGENT_CORDIS_YML, "a second load must overwrite the stale copy");
486
+ test("Podman-ops preset declares its composition as plugin rows", () => {
487
+ const patch = podmanOpsPatch();
488
+ for (const plugin of [
489
+ "@deepseek-ai/dsh-persona",
490
+ "@deepseek-ai/dsh-tool-ask-user",
491
+ "@deepseek-ai/dsh-tool-todo",
492
+ "@deepseek-ai/dsh-tool-web",
493
+ ]) {
494
+ assert.ok(patch.includes(plugin), `podman-ops composition must declare ${plugin}`);
495
+ }
496
+ assert.ok(patch.includes("@deepseek-ai/dsh-agent-preset"), "the preset must be declared through the agent-preset row");
417
497
  });
418
498
  test("summarizeArgs includes PATH additions for container start/recreate", () => {
419
499
  assert.equal(summarizeArgs("container_start", { container: "web", paths: ["/opt/bin"] }), 'Start container "web" with PATH additions: "/opt/bin".');
@@ -443,7 +523,18 @@ test("summarizeArgs includes env keys for container start/recreate", () => {
443
523
  }), 'Start container "web" from image "localhost/dsh-podman/nginx:latest" with mounts: project "team" (read-only) with env: A, B, C.');
444
524
  assert.equal(summarizeArgs("container_start", {
445
525
  container: "web",
446
- env: { A: "1", B: "2", C: "3", D: "4", E: "5", F: "6", G: "7", H: "8", I: "9", J: "10" },
526
+ env: {
527
+ A: "1",
528
+ B: "2",
529
+ C: "3",
530
+ D: "4",
531
+ E: "5",
532
+ F: "6",
533
+ G: "7",
534
+ H: "8",
535
+ I: "9",
536
+ J: "10",
537
+ },
447
538
  }), 'Start container "web" with env: A, B, C, D, E, F, G, H, +2 more.');
448
539
  assert.equal(summarizeArgs("container_recreate", {
449
540
  container: "c",
@@ -460,3 +551,43 @@ test("approval prompts name the resolved path", () => {
460
551
  assert.equal(summarizeArgs("container_write", { container: "c", file_path: "notes.md" }), 'Write "notes.md" in container "c".');
461
552
  assert.equal(summarizeArgs("container_write", { container: "c", file_path: "../x" }, "/projects/team"), 'Write "../x" in container "c".');
462
553
  });
554
+ test("summarizeArgs includes secret env names for container start/recreate", () => {
555
+ assert.equal(summarizeArgs("container_start", {
556
+ container: "web",
557
+ secretEnv: { TOKEN: "api-token" },
558
+ }), 'Start container "web" with secret env: TOKEN.');
559
+ assert.equal(summarizeArgs("container_recreate", {
560
+ container: "c",
561
+ env: { A: "1" },
562
+ secretEnv: { TOKEN: "api-token" },
563
+ }), 'Recreate container "c" with env: A with secret env: TOKEN.');
564
+ // An empty map is not an attachment and is not named.
565
+ assert.equal(summarizeArgs("container_start", { container: "web", secretEnv: {} }), 'Start container "web".');
566
+ });
567
+ test("container_start asks for approval when it attaches secrets", () => {
568
+ // Plain env, paths, and a bare start stay prompt-free by design: the caller
569
+ // already knows those values, unlike a secret the container injects.
570
+ assert.equal(approvalDecision("container_start", { container: "web" }), undefined);
571
+ assert.equal(approvalDecision("container_start", {
572
+ container: "web",
573
+ paths: ["/opt/bin"],
574
+ }), undefined);
575
+ assert.equal(approvalDecision("container_start", { container: "web", env: { A: "1" } }), undefined);
576
+ assert.equal(approvalDecision("container_start", { container: "web", secretEnv: {} }), undefined);
577
+ // Mounts and secret attachments both change what the container can reach.
578
+ assert.equal(approvalDecision("container_start", {
579
+ container: "web",
580
+ mounts: [{ project: "team" }],
581
+ })?.kind, "ask");
582
+ const withSecret = approvalDecision("container_start", {
583
+ container: "web",
584
+ secretEnv: { TOKEN: "api-token" },
585
+ });
586
+ assert.equal(withSecret?.kind, "ask");
587
+ assert.match(withSecret?.reason ?? "", /secret env: TOKEN/);
588
+ });
589
+ test("the bundle patch disables the built-in terminal UI", () => {
590
+ // The Podman terminal replaces the built-in one; its client row must stay off
591
+ // so the guide offers a single terminal card and Ctrl+` is free.
592
+ assert.match(podmanOpsPatch(), /- id: ui-sidebar-terminal\n\s+disabled: true/);
593
+ });
@@ -1,4 +1,5 @@
1
1
  import { type CardSnapshot } from "./client/card-protocol.js";
2
+ import { type ReasonLocale } from "./approval-reasons.js";
2
3
  import { type WorkspaceResolver } from "./workspace-binding.js";
3
4
  /** Build the live snapshot the card renders. */
4
5
  export declare function cardSnapshot(resolver: WorkspaceResolver, workspaceRegistry: any): Promise<CardSnapshot>;
@@ -6,10 +7,10 @@ export declare function cardSnapshot(resolver: WorkspaceResolver, workspaceRegis
6
7
  * Run one card command, returning the notice to show (usually empty). `ctx` is
7
8
  * the settings-injected context, read only for the session locale.
8
9
  */
9
- export declare function runCommand(ctx: any, resolver: WorkspaceResolver, workspaceRegistry: any, raw: Record<string, unknown>): Promise<string>;
10
+ export declare function runCommand(_ctx: any, resolver: WorkspaceResolver, workspaceRegistry: any, raw: Record<string, unknown>, readLocale?: () => ReasonLocale): Promise<string>;
10
11
  /**
11
12
  * Register the card's authenticated fetch route. It is served below the
12
13
  * harness API path, so the carrier applies its Host/Origin fence and browser
13
14
  * authentication before the handler runs.
14
15
  */
15
- export declare function registerCardRoute(ctx: any, resolver: WorkspaceResolver, workspaceRegistry?: any): void;
16
+ export declare function registerCardRoute(ctx: any, resolver: WorkspaceResolver, workspaceRegistry?: any, readLocale?: () => ReasonLocale): void;