@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,127 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { READ_ONLY_GATED_TOOLS, REMOUNT_TOOL_NAME, createReadOnlyShellGate, } from "./index.js";
7
+ import { WORKSPACE_ID } from "./test-support.js";
8
+ const EXEC = {
9
+ name: "bash",
10
+ arguments: {},
11
+ agent: { session: { header: { cwd: "/proj" } } },
12
+ };
13
+ // A resolver whose control plane reports one container with the given mounts,
14
+ // recording every control call; the gate's approval is recorded by reason.
15
+ function gateFixture(mounts, options = {}) {
16
+ const controlCalls = [];
17
+ const approvals = [];
18
+ const containerName = options.containerName ?? "default";
19
+ const resolver = {
20
+ registry: { resolveByPath: async () => ({ id: WORKSPACE_ID }) },
21
+ getConfig: () => ({ projectsRoot: "/projects" }),
22
+ async control(method, request) {
23
+ controlCalls.push([method, request]);
24
+ if (method === "listContainers") {
25
+ return mounts === undefined
26
+ ? { containers: [] }
27
+ : {
28
+ containers: [
29
+ {
30
+ workspaceSlug: WORKSPACE_ID,
31
+ containerName,
32
+ mounts,
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ return {};
38
+ },
39
+ };
40
+ const gate = createReadOnlyShellGate({
41
+ resolver,
42
+ approve: async (_exec, reason) => {
43
+ approvals.push(reason);
44
+ return options.approve ?? true;
45
+ },
46
+ });
47
+ return { gate, controlCalls, approvals };
48
+ }
49
+ test("the gate ignores tools outside its set", async () => {
50
+ const { gate, controlCalls } = gateFixture([]);
51
+ assert.equal(await gate({ ...EXEC, name: "image_list" }, "en"), undefined);
52
+ assert.equal(await gate({ ...EXEC, name: "container_read" }, "en"), undefined);
53
+ assert.deepEqual(controlCalls, []);
54
+ });
55
+ test("the gate covers the shell, file, and daemon tools", () => {
56
+ assert.deepEqual([...READ_ONLY_GATED_TOOLS].sort(), [
57
+ "bash",
58
+ "container_bash",
59
+ "container_edit",
60
+ "container_exec",
61
+ "container_write",
62
+ "daemon_start",
63
+ "edit",
64
+ "pwsh",
65
+ "write",
66
+ ]);
67
+ assert.equal(REMOUNT_TOOL_NAME, "dsh_podman_builtin_remount_read_only");
68
+ });
69
+ test("all mounts read-only allows the tool without a prompt", async () => {
70
+ const { gate, controlCalls, approvals } = gateFixture([
71
+ { projectName: "team", kind: "MOUNT_KIND_PROJECT", mode: "MOUNT_MODE_READ_ONLY" },
72
+ { kind: "MOUNT_KIND_TMPFS", destination: "/scratch", mode: "MOUNT_MODE_READ_WRITE" },
73
+ { kind: "MOUNT_KIND_SECRET", secret: "tls", destination: "/run/secrets/tls" },
74
+ ]);
75
+ assert.deepEqual(await gate(EXEC, "en"), { kind: "allow" });
76
+ assert.deepEqual(approvals, []);
77
+ assert.equal(controlCalls.filter(([method]) => method === "recreateContainer").length, 0);
78
+ });
79
+ test("a read-write mount prompts and then remounts read-only", async () => {
80
+ const { gate, controlCalls, approvals } = gateFixture([
81
+ { projectName: "team", kind: "MOUNT_KIND_PROJECT", mode: "MOUNT_MODE_READ_WRITE" },
82
+ { kind: "MOUNT_KIND_VOLUME", volume: "data", destination: "/data", mode: "MOUNT_MODE_READ_WRITE" },
83
+ { kind: "MOUNT_KIND_TMPFS", destination: "/scratch", mode: "MOUNT_MODE_READ_WRITE" },
84
+ { kind: "MOUNT_KIND_SECRET", secret: "tls", destination: "/run/secrets/tls" },
85
+ ]);
86
+ assert.deepEqual(await gate(EXEC, "en"), { kind: "allow" });
87
+ assert.equal(approvals.length, 1);
88
+ assert.match(approvals[0], /Read-only mode blocks "bash"/);
89
+ assert.match(approvals[0], /- project "team" \(read-write\)/);
90
+ assert.match(approvals[0], /- volume "data" at "\/data" \(read-write\)/);
91
+ assert.match(approvals[0], /Kept as-is: tmpfs at "\/scratch"/);
92
+ const recreate = controlCalls.filter(([method]) => method === "recreateContainer");
93
+ assert.equal(recreate.length, 1);
94
+ assert.deepEqual(recreate[0][1], {
95
+ workspaceSlug: WORKSPACE_ID,
96
+ container: "default",
97
+ mounts: [
98
+ { projectName: "team", kind: "MOUNT_KIND_PROJECT", mode: "MOUNT_MODE_READ_ONLY" },
99
+ { projectName: "", kind: "MOUNT_KIND_VOLUME", mode: "MOUNT_MODE_READ_ONLY", destination: "/data", volume: "data" },
100
+ { projectName: "", kind: "MOUNT_KIND_TMPFS", mode: "MOUNT_MODE_READ_WRITE", destination: "/scratch" },
101
+ { projectName: "", kind: "MOUNT_KIND_SECRET", destination: "/run/secrets/tls", secret: "tls" },
102
+ ],
103
+ });
104
+ });
105
+ test("a declined prompt denies without remounting", async () => {
106
+ const { gate, controlCalls, approvals } = gateFixture([{ projectName: "team", kind: "MOUNT_KIND_PROJECT", mode: "MOUNT_MODE_READ_WRITE" }], { approve: false });
107
+ const decision = (await gate(EXEC, "en"));
108
+ assert.equal(decision.kind, "deny");
109
+ assert.match(decision.reason, /mounts were not remounted/);
110
+ assert.equal(approvals.length, 1);
111
+ assert.equal(controlCalls.filter(([method]) => method === "recreateContainer").length, 0);
112
+ });
113
+ test("an unreadable container falls through to the plain denial", async () => {
114
+ const { gate, approvals } = gateFixture(undefined);
115
+ assert.equal(await gate(EXEC, "en"), undefined);
116
+ assert.deepEqual(approvals, []);
117
+ });
118
+ test("a plugin tool targets its own container", async () => {
119
+ const { gate, controlCalls } = gateFixture([{ projectName: "team", kind: "MOUNT_KIND_PROJECT", mode: "MOUNT_MODE_READ_ONLY" }], { containerName: "web" });
120
+ const exec = {
121
+ name: "container_bash",
122
+ arguments: { container: "web" },
123
+ agent: { session: { header: { cwd: "/proj" } } },
124
+ };
125
+ assert.deepEqual(await gate(exec, "en"), { kind: "allow" });
126
+ assert.equal(controlCalls[0][0], "listContainers");
127
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,181 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { MOUNT_EXEC, SECRET_TOOLS, WORKSPACE_ID, mountRequestRecorder } from "./test-support.js";
7
+ import { READ_ONLY_TOOLS, TOOLS, toolHandlers } from "./index.js";
8
+ test("secret tools are registered with the expected schemas", () => {
9
+ assert.ok(READ_ONLY_TOOLS.has("secret_list"), "secret_list must be read-only");
10
+ const listTool = TOOLS.find((entry) => entry.name === "secret_list");
11
+ assert.ok(listTool, "secret_list registered");
12
+ assert.notEqual(listTool.approval, true, "secret_list must not require approval");
13
+ assert.deepEqual(listTool.parameters.required, []);
14
+ const createTool = TOOLS.find((entry) => entry.name === "secret_create");
15
+ assert.ok(createTool, "secret_create registered");
16
+ assert.notEqual(createTool.approval, true, "secret_create must not require approval");
17
+ assert.deepEqual(createTool.parameters.required, ["name"]);
18
+ assert.equal(createTool.parameters.properties.length.type, "integer");
19
+ assert.equal(createTool.parameters.properties.length.minimum, 1);
20
+ assert.deepEqual(createTool.parameters.properties.charset.enum, [
21
+ "alphanumeric",
22
+ "hex",
23
+ "base64url",
24
+ ]);
25
+ const removeTool = TOOLS.find((entry) => entry.name === "secret_remove");
26
+ assert.ok(removeTool, "secret_remove registered");
27
+ assert.equal(removeTool.approval, true, "secret_remove must require approval");
28
+ assert.deepEqual(removeTool.parameters.required, ["name"]);
29
+ const addTool = TOOLS.find((entry) => entry.name === "container_secret_add");
30
+ assert.ok(addTool, "container_secret_add registered");
31
+ assert.equal(addTool.approval, true, "container_secret_add must require approval");
32
+ assert.deepEqual(addTool.parameters.required, ["container", "env", "secret"]);
33
+ assert.equal(addTool.parameters.properties.secret.type, "string");
34
+ assert.equal(addTool.parameters.properties.env.type, "string");
35
+ const removeEnvTool = TOOLS.find((entry) => entry.name === "container_secret_remove");
36
+ assert.ok(removeEnvTool, "container_secret_remove registered");
37
+ assert.equal(removeEnvTool.approval, true, "container_secret_remove must require approval");
38
+ assert.deepEqual(removeEnvTool.parameters.required, ["container", "env"]);
39
+ for (const name of SECRET_TOOLS) {
40
+ const tool = TOOLS.find((entry) => entry.name === name);
41
+ assert.ok(tool, `${name} registered`);
42
+ assert.equal(tool.parameters.type, "object", `${name} type`);
43
+ assert.equal(typeof tool.parameters.properties, "object");
44
+ assert.ok(Array.isArray(tool.parameters.required));
45
+ }
46
+ });
47
+ test("secret mount kinds forward the secret to the orchestrator", async () => {
48
+ const requests = [];
49
+ const resolver = {
50
+ registry: {
51
+ resolveByPath: async () => ({ id: WORKSPACE_ID }),
52
+ },
53
+ getConfig: () => ({ projectsRoot: "/projects" }),
54
+ async control(method, request) {
55
+ if (method === "addContainerMount" || method === "removeContainerMount") {
56
+ requests.push(request);
57
+ }
58
+ return {};
59
+ },
60
+ };
61
+ const exec = { agent: { session: { header: { cwd: "/proj" } } } };
62
+ await toolHandlers.container_mount_add(resolver, {
63
+ container: "valkey-ctr",
64
+ kind: "secret",
65
+ secret: "valkey-tls",
66
+ destination: "/run/secrets/tls",
67
+ }, exec);
68
+ await toolHandlers.container_mount_remove(resolver, {
69
+ container: "valkey-ctr",
70
+ kind: "secret",
71
+ secret: "valkey-tls",
72
+ }, exec);
73
+ assert.deepEqual(requests[0], {
74
+ workspaceSlug: WORKSPACE_ID,
75
+ container: "valkey-ctr",
76
+ kind: "MOUNT_KIND_SECRET",
77
+ secret: "valkey-tls",
78
+ destination: "/run/secrets/tls",
79
+ });
80
+ assert.deepEqual(requests[1], {
81
+ workspaceSlug: WORKSPACE_ID,
82
+ container: "valkey-ctr",
83
+ kind: "MOUNT_KIND_SECRET",
84
+ secret: "valkey-tls",
85
+ });
86
+ });
87
+ test("mount tools reject a read-write secret mount", async () => {
88
+ const { requests, resolver } = mountRequestRecorder();
89
+ await assert.rejects(() => toolHandlers.container_mount_add(resolver, { container: "web", kind: "secret", secret: "tls", destination: "/run/secrets/tls", mode: "read_write" }, MOUNT_EXEC), /secret mounts are read-only/);
90
+ await assert.rejects(() => toolHandlers.container_start(resolver, {
91
+ container: "web",
92
+ image: "img1",
93
+ mounts: [{ kind: "secret", secret: "tls", destination: "/run/secrets/tls", mode: "read_write" }],
94
+ }, MOUNT_EXEC), /secret mounts are read-only/);
95
+ assert.deepEqual(requests, [], "a rejected mount must not reach the orchestrator");
96
+ });
97
+ test("container_list returns secret_env maps on container rows", async () => {
98
+ const resolver = {
99
+ registry: {
100
+ resolveByPath: async () => ({ id: WORKSPACE_ID }),
101
+ },
102
+ control: async (method) => {
103
+ if (method === "listContainers") {
104
+ return {
105
+ containers: [
106
+ {
107
+ workspaceSlug: WORKSPACE_ID,
108
+ containerName: "default",
109
+ status: "running",
110
+ imageId: "img-1",
111
+ secretEnv: { REDIS_PASSWORD: "db-pass" },
112
+ },
113
+ {
114
+ workspaceSlug: WORKSPACE_ID,
115
+ containerName: "db",
116
+ status: "running",
117
+ secretEnv: {
118
+ A: "a",
119
+ B: "b",
120
+ C: "c",
121
+ D: "d",
122
+ E: "e",
123
+ F: "f",
124
+ G: "g",
125
+ H: "h",
126
+ I: "i",
127
+ J: "j",
128
+ },
129
+ },
130
+ {
131
+ workspaceSlug: WORKSPACE_ID,
132
+ containerName: "worker",
133
+ status: "stopped",
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ return {};
139
+ },
140
+ };
141
+ const exec = { agent: { session: { header: { cwd: "/proj" } } } };
142
+ const out = (await toolHandlers.container_list(resolver, {}, exec));
143
+ assert.deepEqual(out, [
144
+ {
145
+ containerName: "default",
146
+ status: "running",
147
+ imageId: "img-1",
148
+ mounts: [],
149
+ paths: [],
150
+ env: {},
151
+ secretEnv: { REDIS_PASSWORD: "db-pass" },
152
+ },
153
+ {
154
+ containerName: "db",
155
+ status: "running",
156
+ mounts: [],
157
+ paths: [],
158
+ env: {},
159
+ secretEnv: {
160
+ A: "a",
161
+ B: "b",
162
+ C: "c",
163
+ D: "d",
164
+ E: "e",
165
+ F: "f",
166
+ G: "g",
167
+ H: "h",
168
+ I: "i",
169
+ J: "j",
170
+ },
171
+ },
172
+ {
173
+ containerName: "worker",
174
+ status: "stopped",
175
+ mounts: [],
176
+ paths: [],
177
+ env: {},
178
+ secretEnv: {},
179
+ },
180
+ ]);
181
+ });
@@ -0,0 +1 @@
1
+ export {};