@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
package/README.md CHANGED
@@ -9,6 +9,7 @@ SPDX-License-Identifier: MIT
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/@exagone313/dsh-podman)](https://www.npmjs.com/package/@exagone313/dsh-podman)
11
11
  [![GitHub release](https://img.shields.io/github/v/release/Exagone313/dsh-podman)](https://github.com/Exagone313/dsh-podman/releases)
12
+ [![dsh](https://img.shields.io/badge/dsh-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Fdsh)
12
13
  [![orchestrator](https://img.shields.io/badge/orchestrator-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Forchestrator)
13
14
  [![guest-agent](https://img.shields.io/badge/guest--agent-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Fguest-agent)
14
15
 
@@ -20,21 +21,32 @@ English | [中文](README.zh.md)
20
21
  dsh-podman routes all shell execution and file access through disposable and
21
22
  per-project Podman containers.
22
23
 
24
+ ## Features
25
+
26
+ - Disposable, per-project containers
27
+ - Additional containers per workspace
28
+ - Create container images based on Arch Linux, Ubuntu and Alpine Linux
29
+ - Approval-aware escalation
30
+ - Run processes in containers (daemons)
31
+ - Run a terminal in containers
32
+
23
33
  ## Documentation
24
34
 
25
35
  - **[Install DeepSeek Harness & dsh-podman with rootless Podman](docs/install-dsh-and-dsh-podman.md)**
36
+ - [Update](docs/update.md)
26
37
  - [Architecture](docs/architecture.md)
27
38
  - [Configuration](docs/configuration.md)
28
39
  - [Usage](docs/usage.md)
29
40
  - [Development](docs/development.md)
41
+ - [Uninstall](docs/uninstall.md)
30
42
 
31
43
  ## AI-usage disclosure
32
44
 
33
45
  This repository was written in major parts using AI models:
34
46
 
47
+ - DeepSeek Flash
35
48
  - early commits using OpenAI GPT-5.6 Luna
36
- - DeepSeek V4 Flash
37
- - source code weakness fixes by Anthropic Claude Opus 5
49
+ - a few fixes using Anthropic Claude Opus 5
38
50
 
39
51
  ## License
40
52
 
package/README.zh.md CHANGED
@@ -9,6 +9,7 @@ SPDX-License-Identifier: MIT
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/@exagone313/dsh-podman)](https://www.npmjs.com/package/@exagone313/dsh-podman)
11
11
  [![GitHub release](https://img.shields.io/github/v/release/Exagone313/dsh-podman)](https://github.com/Exagone313/dsh-podman/releases)
12
+ [![dsh](https://img.shields.io/badge/dsh-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Fdsh)
12
13
  [![orchestrator](https://img.shields.io/badge/orchestrator-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Forchestrator)
13
14
  [![guest-agent](https://img.shields.io/badge/guest--agent-GHCR-blue)](https://github.com/Exagone313/dsh-podman/pkgs/container/dsh-podman%2Fguest-agent)
14
15
 
@@ -20,21 +21,32 @@ SPDX-License-Identifier: MIT
20
21
  dsh-podman 将所有的 shell 执行与文件访问都路由到一次性、按项目隔离的 Podman
21
22
  容器中。
22
23
 
24
+ ## 特性
25
+
26
+ - 一次性、按项目隔离的容器
27
+ - 每个工作区可添加额外的容器
28
+ - 创建基于 Arch Linux、Ubuntu 和 Alpine Linux 的容器镜像
29
+ - 感知审批的权限提升
30
+ - 在容器中运行进程(守护进程)
31
+ - 在容器中运行终端
32
+
23
33
  ## 文档
24
34
 
25
35
  - **[使用 rootless Podman 安装 DeepSeek Harness 与 dsh-podman](docs/install-dsh-and-dsh-podman.zh.md)**
36
+ - [更新](docs/update.zh.md)
26
37
  - [架构](docs/architecture.zh.md)
27
38
  - [配置](docs/configuration.zh.md)
28
39
  - [使用](docs/usage.zh.md)
29
40
  - [开发](docs/development.zh.md)
41
+ - [卸载](docs/uninstall.zh.md)
30
42
 
31
43
  ## AI 使用声明
32
44
 
33
45
  本仓库的大部分代码由 AI 模型编写:
34
46
 
47
+ - DeepSeek Flash
35
48
  - 早期提交使用 OpenAI GPT-5.6 Luna
36
- - DeepSeek V4 Flash
37
- - 源代码弱点修复由 Anthropic Claude Opus 5 完成
49
+ - 少量修复由 Anthropic Claude Opus 5 完成
38
50
 
39
51
  ## 许可证
40
52
 
package/cordis.patch.yml CHANGED
@@ -12,6 +12,64 @@
12
12
  disabled: true
13
13
  - id: spill-local
14
14
  disabled: true
15
+ # disable built-in terminal.
16
+ - id: ui-sidebar-terminal
17
+ disabled: true
15
18
  - insert:
16
19
  - id: podman
17
20
  name: '@exagone313/dsh-podman'
21
+ # The "Podman operator mode" agent preset, declared as a row so the
22
+ # preset registry serves it. Its id is the approval policy's preset key
23
+ # (PODMAN_OPS_PRESET in src/approval.ts), and the composition below is the
24
+ # agent's plugin set.
25
+ - id: preset-podman-ops
26
+ name: '@deepseek-ai/dsh-agent-preset'
27
+ config:
28
+ id: podman-ops
29
+ name: Podman operator mode
30
+ description: >-
31
+ Podman-focused container operations — manage images, containers,
32
+ volumes, mounts, secrets, and daemons. Inspections and lifecycle run
33
+ directly; commands, container file edits, daemon starts, and secret
34
+ changes ask for approval; supports web research.
35
+ order: 2
36
+ plugins:
37
+ - id: persona
38
+ name: '@deepseek-ai/dsh-persona'
39
+ config:
40
+ suffix: Your working directory is {{cwd}}.
41
+ prefix: >-
42
+ You are a Podman container-operations agent powered by the {{model}} model.
43
+
44
+ You manage container infrastructure through the dsh-podman tools: images
45
+ (image_list, image_get, image_build, image_rebuild, image_rebuild_all,
46
+ image_remove), containers (container_list, container_start,
47
+ container_recreate, container_remove), mounts (container_mount_list,
48
+ container_mount_add, container_mount_remove, container_mount_update),
49
+ PATH additions (container_path_set, container_path_add,
50
+ container_path_remove), volumes (volume_list,
51
+ volume_create, volume_remove), secrets (secret_list, secret_create,
52
+ secret_remove, container_secret_add, container_secret_remove), daemons
53
+ (daemon_list, daemon_start, daemon_stop, daemon_restart, daemon_logs),
54
+ and container inspection (container_read, container_glob,
55
+ container_grep).
56
+
57
+ Inspection and lifecycle operations run directly. Running commands inside
58
+ a container (container_bash, container_exec), editing container files
59
+ (container_write, container_edit), starting daemons (daemon_start),
60
+ exposing or removing secret environment variables (container_secret_add,
61
+ container_secret_remove), removing secrets (secret_remove), and
62
+ rebuilding all images (image_rebuild_all) require the user's approval.
63
+ Use web_search to research images and documentation. Use
64
+ ask_user_question for user decisions and todo_write to track work.
65
+ - id: tool-ask-user
66
+ name: '@deepseek-ai/dsh-tool-ask-user'
67
+ - id: tool-todo
68
+ name: '@deepseek-ai/dsh-tool-todo'
69
+ config:
70
+ allowParallelInProgress: true
71
+ - id: tool-web
72
+ name: '@deepseek-ai/dsh-tool-web'
73
+ config:
74
+ fetch: false
75
+ searchTimeoutMs: 60000
@@ -24,6 +24,7 @@ export type ReasonFact = {
24
24
  image?: string;
25
25
  mounts?: readonly MountFact[];
26
26
  env?: readonly string[];
27
+ secretEnv?: readonly string[];
27
28
  paths?: readonly string[];
28
29
  } | {
29
30
  kind: "container_remove";
@@ -119,11 +120,9 @@ export type DenialFact = {
119
120
  source?: string;
120
121
  mirror?: string;
121
122
  };
122
- export interface SettingsReader {
123
- get(ns: string): unknown;
124
- }
125
- export declare function resolveReasonLocale(settings: SettingsReader | undefined): ReasonLocale;
123
+ export declare function resolveReasonLocale(value: unknown): ReasonLocale;
126
124
  export declare function reasonLocale(value: unknown): ReasonLocale;
127
125
  export declare function renderReason(locale: ReasonLocale, fact: ReasonFact): string;
128
126
  export declare function renderCacheCleanNotice(locale: ReasonLocale, files: number): string;
127
+ export declare function renderDefaultEnvSyncNotice(locale: ReasonLocale, applied: number, skipped: number): string;
129
128
  export declare function renderDenial(locale: ReasonLocale, fact: DenialFact): string;
@@ -1,20 +1,11 @@
1
1
  // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
- import { CONTAINER_NS } from "./settings-schema.js";
5
- // Settings namespace owned by the browser locale plugin; only read here.
6
- const LOCALE_NS = "locale";
7
- const LOCALE_PREFERENCE_FIELD = "preference";
8
- // The locale to render approval text in: the client-observed active locale
9
- // first (it also captures the browser default), then the durable user
10
- // preference, then English.
11
- export function resolveReasonLocale(settings) {
12
- if (settings === undefined)
13
- return "en";
14
- const observed = field(settings.get(CONTAINER_NS), "uiLocale");
15
- if (typeof observed === "string" && observed !== "")
16
- return reasonLocale(observed);
17
- return reasonLocale(field(settings.get(LOCALE_NS), LOCALE_PREFERENCE_FIELD));
4
+ // The locale to render approval text in, from the plugin's own volatile
5
+ // `uiLocale` preference (the browser client writes it); an unset value renders
6
+ // in English.
7
+ export function resolveReasonLocale(value) {
8
+ return reasonLocale(value);
18
9
  }
19
10
  // Normalize a BCP 47-style locale id to a shipped language; `zh-Hans`, `zh_CN`
20
11
  // and friends all map to `zh`, everything else to English.
@@ -24,11 +15,6 @@ export function reasonLocale(value) {
24
15
  const primary = value.toLowerCase().split(/[-_]/, 1)[0];
25
16
  return primary === "zh" ? "zh" : "en";
26
17
  }
27
- function field(value, key) {
28
- return typeof value === "object" && value !== null
29
- ? value[key]
30
- : undefined;
31
- }
32
18
  // Pick the locale's variant of one string.
33
19
  function pick(locale, en, zh) {
34
20
  return locale === "zh" ? zh : en;
@@ -68,9 +54,7 @@ function mountLabel(locale, mount) {
68
54
  // undefined when it has none. The caller supplies the surrounding punctuation,
69
55
  // since the locales wrap it differently.
70
56
  function mountDestinationText(locale, mount) {
71
- return mount.destination === undefined
72
- ? undefined
73
- : pick(locale, `at ${quoted(locale, mount.destination)}`, `挂载到 ${quoted(locale, mount.destination)}`);
57
+ return mount.destination === undefined ? undefined : pick(locale, `at ${quoted(locale, mount.destination)}`, `挂载到 ${quoted(locale, mount.destination)}`);
74
58
  }
75
59
  // Render a mount's source and destination: `volume "data" at "/data"`.
76
60
  function mountSourceText(locale, mount) {
@@ -94,7 +78,9 @@ function mountModeName(locale, mount) {
94
78
  // Render one mount as `volume "data" at "/data" (read-only)`.
95
79
  function mountText(locale, mount) {
96
80
  const mode = mountModeName(locale, mount);
97
- const suffix = mode === undefined ? "" : pick(locale, ` (${mode})`, `(${mode})`);
81
+ const suffix = mode === undefined
82
+ ? ""
83
+ : pick(locale, ` (${mode})`, `(${mode})`);
98
84
  return `${mountSourceText(locale, mount)}${suffix}`;
99
85
  }
100
86
  // Render a command word list, capped at 8 words with an ellipsis tail.
@@ -114,26 +100,25 @@ function identityText(locale, fact) {
114
100
  if (fact.groups !== undefined && fact.groups.length > 0) {
115
101
  ids.push(`groups ${joinList(locale, fact.groups.map((group) => String(group)))}`);
116
102
  }
117
- return ids.length === 0 ? "" : pick(locale, ` (${ids.join(", ")})`, `(${ids.join("、")})`);
103
+ return ids.length === 0
104
+ ? ""
105
+ : pick(locale, ` (${ids.join(", ")})`, `(${ids.join("、")})`);
118
106
  }
119
107
  // Start/recreate share one shape; only the verb differs.
120
108
  function containerRun(locale, fact) {
121
- const image = fact.image === undefined
122
- ? ""
123
- : pick(locale, ` from image ${quoted(locale, fact.image)}`, `(镜像 ${quoted(locale, fact.image)})`);
109
+ const image = fact.image === undefined ? "" : pick(locale, ` from image ${quoted(locale, fact.image)}`, `(镜像 ${quoted(locale, fact.image)})`);
124
110
  const mounts = fact.mounts === undefined || fact.mounts.length === 0
125
111
  ? ""
126
112
  : pick(locale, ` with mounts: ${joinList(locale, fact.mounts.map((mount) => mountText(locale, mount)))}`, `,挂载:${joinList(locale, fact.mounts.map((mount) => mountText(locale, mount)))}`);
127
- const env = fact.env === undefined || fact.env.length === 0
128
- ? ""
129
- : pick(locale, ` with env: ${joinList(locale, fact.env)}`, `,环境变量:${joinList(locale, fact.env)}`);
130
- const paths = fact.paths === undefined || fact.paths.length === 0
113
+ const env = fact.env === undefined || fact.env.length === 0 ? "" : pick(locale, ` with env: ${joinList(locale, fact.env)}`, `,环境变量:${joinList(locale, fact.env)}`);
114
+ const secretEnv = fact.secretEnv === undefined || fact.secretEnv.length === 0
131
115
  ? ""
132
- : pick(locale, ` with PATH additions: ${joinList(locale, fact.paths.map((path) => quoted(locale, path)))}`, `,PATH 附加项:${joinList(locale, fact.paths.map((path) => quoted(locale, path)))}`);
116
+ : pick(locale, ` with secret env: ${joinList(locale, fact.secretEnv)}`, `,机密环境变量:${joinList(locale, fact.secretEnv)}`);
117
+ const paths = fact.paths === undefined || fact.paths.length === 0 ? "" : pick(locale, ` with PATH additions: ${joinList(locale, fact.paths.map((path) => quoted(locale, path)))}`, `,PATH 附加项:${joinList(locale, fact.paths.map((path) => quoted(locale, path)))}`);
133
118
  const verb = fact.kind === "container_start"
134
119
  ? pick(locale, "Start", "启动")
135
120
  : pick(locale, "Recreate", "重建");
136
- return pick(locale, `${verb} container ${quoted(locale, fact.container)}${image}${mounts}${env}${paths}.`, `${verb}容器 ${quoted(locale, fact.container)}${image}${mounts}${env}${paths}。`);
121
+ return pick(locale, `${verb} container ${quoted(locale, fact.container)}${image}${mounts}${env}${secretEnv}${paths}.`, `${verb}容器 ${quoted(locale, fact.container)}${image}${mounts}${env}${secretEnv}${paths}。`);
137
122
  }
138
123
  // Render one reason as the single-line headline shown in the approval prompt.
139
124
  // Reasons that end in a raw command keep no trailing period, so the command
@@ -141,12 +126,8 @@ function containerRun(locale, fact) {
141
126
  export function renderReason(locale, fact) {
142
127
  switch (fact.kind) {
143
128
  case "image_build": {
144
- const parent = fact.parent === undefined
145
- ? ""
146
- : pick(locale, ` from ${quoted(locale, fact.parent)}`, `(基于 ${quoted(locale, fact.parent)})`);
147
- const packages = fact.packages === undefined
148
- ? ""
149
- : pick(locale, ` with packages: ${joinList(locale, fact.packages)}`, `,软件包:${joinList(locale, fact.packages)}`);
129
+ const parent = fact.parent === undefined ? "" : pick(locale, ` from ${quoted(locale, fact.parent)}`, `(基于 ${quoted(locale, fact.parent)})`);
130
+ const packages = fact.packages === undefined ? "" : pick(locale, ` with packages: ${joinList(locale, fact.packages)}`, `,软件包:${joinList(locale, fact.packages)}`);
150
131
  return pick(locale, `Build image ${quoted(locale, fact.image)}${parent}${packages}.`, `构建镜像 ${quoted(locale, fact.image)}${parent}${packages}。`);
151
132
  }
152
133
  case "image_rebuild":
@@ -188,9 +169,7 @@ export function renderReason(locale, fact) {
188
169
  const remount = fact.remount
189
170
  .map((mount) => `- ${mountText(locale, mount)}`)
190
171
  .join("\n");
191
- const keep = fact.keep.length === 0
192
- ? ""
193
- : pick(locale, `\n\nKept as-is: ${joinList(locale, fact.keep.map((mount) => mountText(locale, mount)))}.`, `\n\n保持不变:${joinList(locale, fact.keep.map((mount) => mountText(locale, mount)))}。`);
172
+ const keep = fact.keep.length === 0 ? "" : pick(locale, `\n\nKept as-is: ${joinList(locale, fact.keep.map((mount) => mountText(locale, mount)))}.`, `\n\n保持不变:${joinList(locale, fact.keep.map((mount) => mountText(locale, mount)))}。`);
194
173
  return pick(locale, `Read-only mode blocks ${quoted(locale, fact.tool)} while a mount is read-write. Remount these mounts read-only to run it:\n\n${remount}${keep}`, `只读模式在存在读写挂载时会阻止 ${quoted(locale, fact.tool)}。将这些挂载重新挂载为只读即可运行:\n\n${remount}${keep}`);
195
174
  }
196
175
  case "volume_remove":
@@ -202,16 +181,12 @@ export function renderReason(locale, fact) {
202
181
  case "container_secret_remove":
203
182
  return pick(locale, `Detach secret env var ${quoted(locale, fact.env)} from container ${quoted(locale, fact.container)}.`, `从容器 ${quoted(locale, fact.container)} 中移除机密环境变量 ${quoted(locale, fact.env)}。`);
204
183
  case "container_bash": {
205
- const cwd = fact.cwd === undefined
206
- ? ""
207
- : pick(locale, ` (cwd ${quoted(locale, fact.cwd)})`, `(工作目录 ${quoted(locale, fact.cwd)})`);
184
+ const cwd = fact.cwd === undefined ? "" : pick(locale, ` (cwd ${quoted(locale, fact.cwd)})`, `(工作目录 ${quoted(locale, fact.cwd)})`);
208
185
  const identity = identityText(locale, fact);
209
186
  return pick(locale, `Run a shell command in container ${quoted(locale, fact.container)}${cwd}: ${fact.command}${identity}`, `在容器 ${quoted(locale, fact.container)}${cwd} 中运行 shell 命令:${fact.command}${identity}`);
210
187
  }
211
188
  case "container_exec": {
212
- const cwd = fact.cwd === undefined
213
- ? ""
214
- : pick(locale, ` (cwd ${quoted(locale, fact.cwd)})`, `(工作目录 ${quoted(locale, fact.cwd)})`);
189
+ const cwd = fact.cwd === undefined ? "" : pick(locale, ` (cwd ${quoted(locale, fact.cwd)})`, `(工作目录 ${quoted(locale, fact.cwd)})`);
215
190
  const identity = identityText(locale, fact);
216
191
  return pick(locale, `Run a command in container ${quoted(locale, fact.container)}${cwd}: ${argvText(fact.argv)}${identity}`, `在容器 ${quoted(locale, fact.container)}${cwd} 中运行命令:${argvText(fact.argv)}${identity}`);
217
192
  }
@@ -220,7 +195,9 @@ export function renderReason(locale, fact) {
220
195
  case "container_edit":
221
196
  return pick(locale, `Edit ${quoted(locale, fact.path)} in container ${quoted(locale, fact.container)}.`, `在容器 ${quoted(locale, fact.container)} 中编辑 ${quoted(locale, fact.path)}。`);
222
197
  case "daemon_start": {
223
- const named = fact.name === undefined ? "" : ` ${quoted(locale, fact.name)}`;
198
+ const named = fact.name === undefined
199
+ ? ""
200
+ : ` ${quoted(locale, fact.name)}`;
224
201
  const suffix = identityText(locale, fact);
225
202
  return pick(locale, `Start daemon${named} in container ${quoted(locale, fact.container)}: ${argvText(fact.argv)}${suffix}`, `在容器 ${quoted(locale, fact.container)} 中启动守护进程${named}:${argvText(fact.argv)}${suffix}`);
226
203
  }
@@ -231,6 +208,20 @@ export function renderReason(locale, fact) {
231
208
  export function renderCacheCleanNotice(locale, files) {
232
209
  return pick(locale, `removed ${files} cached ${files === 1 ? "file" : "files"}`, `已移除 ${files} 个缓存文件`);
233
210
  }
211
+ // Render the settings notice shown after the default environment was applied
212
+ // to existing containers: how many were recreated, and how many stopped
213
+ // containers were left for their next start.
214
+ export function renderDefaultEnvSyncNotice(locale, applied, skipped) {
215
+ const containers = (count) => pick(locale, `${count} container${count === 1 ? "" : "s"}`, `${count} 个容器`);
216
+ if (applied === 0 && skipped === 0) {
217
+ return pick(locale, "the default environment is already applied to every running container", "默认环境已应用于所有运行中的容器");
218
+ }
219
+ if (applied === 0) {
220
+ return pick(locale, `no running container needed the default environment; ${containers(skipped)} stopped`, `没有运行中的容器需要默认环境;已跳过 ${containers(skipped)}`);
221
+ }
222
+ const suffix = skipped === 0 ? "" : pick(locale, `; ${containers(skipped)} stopped, to pick up on their next start`, `;已跳过 ${containers(skipped)},将在下次启动时生效`);
223
+ return pick(locale, `applied the default environment to ${containers(applied)}${suffix}`, `已将默认环境应用于 ${containers(applied)}${suffix}`);
224
+ }
234
225
  // Render one policy denial.
235
226
  export function renderDenial(locale, fact) {
236
227
  switch (fact.kind) {
@@ -3,7 +3,7 @@
3
3
  // SPDX-License-Identifier: MIT
4
4
  import test from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { reasonLocale, renderCacheCleanNotice, renderDenial, renderReason, resolveReasonLocale, } from "./approval-reasons.js";
6
+ import { reasonLocale, renderCacheCleanNotice, renderDefaultEnvSyncNotice, renderDenial, renderReason, resolveReasonLocale, } from "./approval-reasons.js";
7
7
  import { preExecutePolicy, summarizeArgs } from "./index.js";
8
8
  import { testReadSession } from "./test-support.js";
9
9
  test("reasonLocale normalizes a locale id to a shipped language", () => {
@@ -16,16 +16,13 @@ test("reasonLocale normalizes a locale id to a shipped language", () => {
16
16
  assert.equal(reasonLocale(undefined), "en");
17
17
  assert.equal(reasonLocale(42), "en");
18
18
  });
19
- test("resolveReasonLocale prefers the client-observed locale over the preference", () => {
20
- const settings = (container, locale) => ({
21
- get: (ns) => (ns === "podman" ? container : ns === "locale" ? locale : undefined),
22
- });
19
+ test("resolveReasonLocale reads the plugin's recorded locale", () => {
23
20
  assert.equal(resolveReasonLocale(undefined), "en");
24
- assert.equal(resolveReasonLocale(settings({ uiLocale: "zh" }, { preference: "en" })), "zh");
25
- assert.equal(resolveReasonLocale(settings({ uiLocale: "" }, { preference: "zh" })), "zh");
26
- assert.equal(resolveReasonLocale(settings({}, { preference: "zh" })), "zh");
27
- assert.equal(resolveReasonLocale(settings({ uiLocale: "en" }, { preference: "zh" })), "en");
28
- assert.equal(resolveReasonLocale(settings({}, {})), "en");
21
+ assert.equal(resolveReasonLocale(""), "en");
22
+ assert.equal(resolveReasonLocale("zh"), "zh");
23
+ assert.equal(resolveReasonLocale("zh-Hans"), "zh");
24
+ assert.equal(resolveReasonLocale("en"), "en");
25
+ assert.equal(resolveReasonLocale(42), "en");
29
26
  });
30
27
  test("summarizeArgs renders reasons in Chinese when asked", () => {
31
28
  assert.equal(summarizeArgs("container_mount_add", {
@@ -57,7 +54,12 @@ test("renderDenial renders both languages", () => {
57
54
  test("renderReason covers the remaining reason kinds in both languages", () => {
58
55
  assert.equal(renderReason("en", { kind: "image_rebuild_all" }), "Rebuild all images.");
59
56
  assert.equal(renderReason("zh", { kind: "image_rebuild_all" }), "重建全部镜像。");
60
- assert.equal(renderReason("zh", { kind: "daemon_start", container: "c", argv: ["x"], uid: 1 }), "在容器 “c” 中启动守护进程:x(uid 1)");
57
+ assert.equal(renderReason("zh", {
58
+ kind: "daemon_start",
59
+ container: "c",
60
+ argv: ["x"],
61
+ uid: 1,
62
+ }), "在容器 “c” 中启动守护进程:x(uid 1)");
61
63
  assert.equal(renderReason("zh", {
62
64
  kind: "container_bash",
63
65
  container: "c",
@@ -65,11 +67,21 @@ test("renderReason covers the remaining reason kinds in both languages", () => {
65
67
  uid: 1,
66
68
  groups: [3, 4],
67
69
  }), "在容器 “c” 中运行 shell 命令:id(uid 1、groups 3、4)");
68
- assert.equal(renderReason("zh", { kind: "container_secret_add", container: "c", secret: "s", env: "E" }), "将机密 “s” 作为 “E” 注入容器 “c”。");
70
+ assert.equal(renderReason("zh", {
71
+ kind: "container_secret_add",
72
+ container: "c",
73
+ secret: "s",
74
+ env: "E",
75
+ }), "将机密 “s” 作为 “E” 注入容器 “c”。");
69
76
  assert.equal(renderReason("zh", {
70
77
  kind: "container_mount_update",
71
78
  container: "valkey-ctr",
72
- mount: { kind: "volume", source: "valkey-data", destination: "/data", readOnly: false },
79
+ mount: {
80
+ kind: "volume",
81
+ source: "valkey-data",
82
+ destination: "/data",
83
+ readOnly: false,
84
+ },
73
85
  }), "更新容器 “valkey-ctr” 中的挂载:将卷 “valkey-data”(挂载到 “/data”)重新挂载为读写。");
74
86
  assert.equal(renderReason("zh", {
75
87
  kind: "container_mount_update",
@@ -88,7 +100,12 @@ test("renderReason renders the read-only remount plan", () => {
88
100
  tool: "bash",
89
101
  remount: [
90
102
  { kind: "project", source: "team", readOnly: false },
91
- { kind: "volume", source: "data", destination: "/data", readOnly: false },
103
+ {
104
+ kind: "volume",
105
+ source: "data",
106
+ destination: "/data",
107
+ readOnly: false,
108
+ },
92
109
  ],
93
110
  keep: [{ kind: "tmpfs", source: "", destination: "/scratch" }],
94
111
  }), 'Read-only mode blocks "bash" while a mount is read-write. Remount these mounts read-only to run it:\n\n- project "team" (read-write)\n- volume "data" at "/data" (read-write)\n\nKept as-is: tmpfs at "/scratch".');
@@ -103,8 +120,19 @@ test("renderDenial renders the declined remount", () => {
103
120
  assert.equal(renderDenial("en", { kind: "read_only_remount_declined", tool: "bash" }), 'Denied: read-only mode blocks "bash" and the mounts were not remounted.');
104
121
  assert.equal(renderDenial("zh", { kind: "read_only_remount_declined", tool: "bash" }), "已拒绝:只读模式阻止 “bash”,且挂载未被重新挂载。");
105
122
  });
123
+ test("renderDefaultEnvSyncNotice renders both languages", () => {
124
+ assert.equal(renderDefaultEnvSyncNotice("en", 2, 0), "applied the default environment to 2 containers");
125
+ assert.equal(renderDefaultEnvSyncNotice("en", 1, 2), "applied the default environment to 1 container; 2 containers stopped, to pick up on their next start");
126
+ assert.equal(renderDefaultEnvSyncNotice("en", 0, 1), "no running container needed the default environment; 1 container stopped");
127
+ assert.equal(renderDefaultEnvSyncNotice("en", 0, 0), "the default environment is already applied to every running container");
128
+ assert.equal(renderDefaultEnvSyncNotice("zh", 2, 0), "已将默认环境应用于 2 个容器");
129
+ assert.equal(renderDefaultEnvSyncNotice("zh", 0, 1), "没有运行中的容器需要默认环境;已跳过 1 个容器");
130
+ });
106
131
  test("preExecutePolicy renders a localized deny reason", async () => {
107
- const denied = (await preExecutePolicy({ name: "container_start", agent: { session: { facts: { mode: "read-only" } } } }, () => Promise.resolve({ kind: "allow" }), undefined, () => "zh", testReadSession));
132
+ const denied = (await preExecutePolicy({
133
+ name: "container_start",
134
+ agent: { session: { facts: { mode: "read-only" } } },
135
+ }, () => Promise.resolve({ kind: "allow" }), undefined, () => "zh", testReadSession));
108
136
  assert.equal(denied.kind, "deny");
109
137
  assert.equal(denied.reason, "已拒绝:当前会话为只读,而 “container_start” 需要写入权限。");
110
138
  });
package/dist/approval.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
  import { approvalPath, currentCwd } from "./guest-rpc.js";
5
- import { inferMountKind, projectMountDestinationReason } from "./mount-input.js";
5
+ import { inferMountKind, projectMountDestinationReason, } from "./mount-input.js";
6
6
  import { defaultMountMode } from "./mount-enums.js";
7
7
  import { TOOLS } from "./tool-schemas.js";
8
8
  import { renderDenial, renderReason, } from "./approval-reasons.js";
@@ -38,7 +38,9 @@ function mountFact(mount, readOnly) {
38
38
  // Build the locale-independent reason fact for a gated call, or undefined when
39
39
  // the arguments carry too little to describe the call.
40
40
  export function reasonFact(name, args, sessionCwd) {
41
- const str = (key) => typeof args[key] === "string" && args[key] !== "" ? args[key] : undefined;
41
+ const str = (key) => typeof args[key] === "string" && args[key] !== ""
42
+ ? args[key]
43
+ : undefined;
42
44
  const listOf = (key) => {
43
45
  const value = args[key];
44
46
  return Array.isArray(value) && value.length > 0
@@ -62,14 +64,15 @@ export function reasonFact(name, args, sessionCwd) {
62
64
  .filter((item) => item !== undefined);
63
65
  return items.length === 0 ? undefined : items;
64
66
  };
65
- const envKeys = () => {
66
- const value = args.env;
67
+ const mapKeys = (key) => {
68
+ const value = args[key];
67
69
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
68
70
  return undefined;
69
71
  }
70
72
  const keys = Object.keys(value);
71
73
  return keys.length === 0 ? undefined : keys;
72
74
  };
75
+ const envKeys = () => mapKeys("env");
73
76
  const numberListOf = (key) => {
74
77
  const value = args[key];
75
78
  return Array.isArray(value) && value.length > 0
@@ -120,6 +123,7 @@ export function reasonFact(name, args, sessionCwd) {
120
123
  const image = str("image");
121
124
  const mounts = mountItems();
122
125
  const env = envKeys();
126
+ const secretEnv = mapKeys("secretEnv");
123
127
  const paths = listOf("paths");
124
128
  return {
125
129
  kind: name,
@@ -127,20 +131,27 @@ export function reasonFact(name, args, sessionCwd) {
127
131
  ...(image === undefined ? {} : { image }),
128
132
  ...(mounts === undefined ? {} : { mounts }),
129
133
  ...(env === undefined ? {} : { env }),
134
+ ...(secretEnv === undefined ? {} : { secretEnv }),
130
135
  ...(paths === undefined ? {} : { paths }),
131
136
  };
132
137
  }
133
138
  case "container_remove": {
134
139
  const container = str("container");
135
- return container === undefined ? undefined : { kind: "container_remove", container };
140
+ return container === undefined
141
+ ? undefined
142
+ : { kind: "container_remove", container };
136
143
  }
137
144
  case "volume_remove": {
138
145
  const volume = str("name");
139
- return volume === undefined ? undefined : { kind: "volume_remove", name: volume };
146
+ return volume === undefined
147
+ ? undefined
148
+ : { kind: "volume_remove", name: volume };
140
149
  }
141
150
  case "secret_remove": {
142
151
  const secret = str("name");
143
- return secret === undefined ? undefined : { kind: "secret_remove", name: secret };
152
+ return secret === undefined
153
+ ? undefined
154
+ : { kind: "secret_remove", name: secret };
144
155
  }
145
156
  case "container_secret_add": {
146
157
  const container = str("container");
@@ -356,7 +367,9 @@ export async function preExecutePolicy(exec, next, getProjectsRoot, getLocale, r
356
367
  };
357
368
  }
358
369
  const args = exec.arguments;
359
- const parsed = typeof args === "object" && args !== null ? args : undefined;
370
+ const parsed = typeof args === "object" && args !== null
371
+ ? args
372
+ : undefined;
360
373
  const projectsRoot = getProjectsRoot?.();
361
374
  const denyReason = mountDestinationsReason(name, parsed, projectsRoot, locale);
362
375
  if (denyReason !== undefined) {
@@ -366,7 +379,10 @@ export async function preExecutePolicy(exec, next, getProjectsRoot, getLocale, r
366
379
  return next();
367
380
  const sessionCwd = currentCwd(exec);
368
381
  if (facts.preset === PODMAN_OPS_PRESET && PODMAN_OPS_APPROVAL_TOOLS.has(name)) {
369
- return { kind: "ask", ...askReason(name, parsed ?? {}, sessionCwd, locale) };
382
+ return {
383
+ kind: "ask",
384
+ ...askReason(name, parsed ?? {}, sessionCwd, locale),
385
+ };
370
386
  }
371
387
  return approvalDecision(name, parsed, sessionCwd, locale) ?? next();
372
388
  }
@@ -377,7 +393,8 @@ export async function preExecutePolicy(exec, next, getProjectsRoot, getLocale, r
377
393
  export function mountDestinationsReason(name, args, projectsRoot, locale = "en") {
378
394
  if (args === undefined)
379
395
  return undefined;
380
- if (name === "container_mount_add" || name === "container_mount_remove" || name === "container_mount_update") {
396
+ if (name === "container_mount_add" || name === "container_mount_remove" ||
397
+ name === "container_mount_update") {
381
398
  return projectMountDestinationReason(projectsRoot, args, locale);
382
399
  }
383
400
  if (name === "container_start" || name === "container_recreate") {