@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.
- package/README.md +14 -2
- package/README.zh.md +14 -2
- package/cordis.patch.yml +58 -0
- package/dist/approval-reasons.d.ts +3 -4
- package/dist/approval-reasons.js +40 -49
- package/dist/approval-reasons.test.js +43 -15
- package/dist/approval.js +27 -10
- package/dist/approval.test.js +165 -34
- package/dist/card-route.d.ts +3 -2
- package/dist/card-route.js +154 -42
- package/dist/card-route.test.js +85 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +92 -50
- package/dist/client/card-protocol.d.ts +6 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +16 -18
- package/dist/client/container-card-controller.js +92 -40
- package/dist/client/container-card-create-modal.d.ts +2 -2
- package/dist/client/container-card-create-modal.js +13 -5
- package/dist/client/container-card-default-env.d.ts +10 -0
- package/dist/client/container-card-default-env.js +69 -0
- package/dist/client/container-card-directory.d.ts +2 -2
- package/dist/client/container-card-directory.js +12 -4
- package/dist/client/container-card-editors.d.ts +3 -3
- package/dist/client/container-card-editors.js +104 -32
- package/dist/client/container-card-images.d.ts +4 -4
- package/dist/client/container-card-images.js +19 -6
- package/dist/client/container-card-paths.d.ts +3 -3
- package/dist/client/container-card-paths.js +18 -11
- package/dist/client/container-card-row.d.ts +10 -6
- package/dist/client/container-card-row.js +82 -13
- package/dist/client/container-card-secrets.d.ts +3 -3
- package/dist/client/container-card-secrets.js +8 -6
- package/dist/client/container-card-shared.d.ts +4 -17
- package/dist/client/container-card-shared.js +9 -25
- package/dist/client/container-card-styles.d.ts +2 -8
- package/dist/client/container-card-styles.js +14 -52
- package/dist/client/container-card-volumes.d.ts +2 -2
- package/dist/client/container-card-volumes.js +9 -5
- package/dist/client/container-card-workspace.d.ts +8 -6
- package/dist/client/container-card-workspace.js +9 -3
- package/dist/client/directory-picker.js +2 -1
- package/dist/client/index.js +13141 -2036
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +106 -38
- package/dist/client/podman-terminal-guide.d.ts +11 -0
- package/dist/client/podman-terminal-guide.js +208 -0
- package/dist/client/podman-terminal-title.d.ts +7 -0
- package/dist/client/podman-terminal-title.js +30 -0
- package/dist/client/podman-terminal.d.ts +26 -0
- package/dist/client/podman-terminal.js +412 -0
- package/dist/client/read-only-approval.d.ts +2 -2
- package/dist/client/read-only-approval.js +13 -3
- package/dist/client/slot-contract.d.ts +0 -11
- package/dist/client/terminal-preference.d.ts +26 -0
- package/dist/client/terminal-preference.js +67 -0
- package/dist/client/terminal-protocol.d.ts +85 -0
- package/dist/client/terminal-protocol.js +14 -0
- package/dist/client/terminal-tab.d.ts +60 -0
- package/dist/client/terminal-tab.js +35 -0
- package/dist/client/terminal-targets.d.ts +33 -0
- package/dist/client/terminal-targets.js +65 -0
- package/dist/client/terminal-titles.d.ts +23 -0
- package/dist/client/terminal-titles.js +52 -0
- package/dist/client/terminal-transport.d.ts +63 -0
- package/dist/client/terminal-transport.js +207 -0
- package/dist/client/tool-views.js +215 -49
- package/dist/container-env.d.ts +11 -0
- package/dist/container-env.js +96 -0
- package/dist/container-env.test.d.ts +1 -0
- package/dist/container-env.test.js +93 -0
- package/dist/containers.test.js +196 -18
- package/dist/daemons.test.js +18 -4
- package/dist/dsh-version.d.ts +1 -0
- package/dist/dsh-version.js +46 -0
- package/dist/env-rows.d.ts +14 -0
- package/dist/env-rows.js +52 -0
- package/dist/env-rows.test.d.ts +1 -0
- package/dist/env-rows.test.js +43 -0
- package/dist/fs-provider.d.ts +1 -0
- package/dist/fs-provider.js +26 -12
- package/dist/fs-provider.test.js +51 -10
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
- package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
- package/dist/grpc/runtime-client.d.ts +3 -1
- package/dist/grpc/runtime-client.js +90 -17
- package/dist/guest-rpc.d.ts +15 -19
- package/dist/guest-rpc.js +289 -70
- package/dist/guest-rpc.test.d.ts +1 -0
- package/dist/guest-rpc.test.js +241 -0
- package/dist/guest-terminal.d.ts +30 -0
- package/dist/guest-terminal.js +196 -0
- package/dist/images.test.js +10 -3
- package/dist/index.d.ts +11 -16
- package/dist/index.js +42 -36
- package/dist/locales.test.d.ts +1 -0
- package/dist/locales.test.js +34 -0
- package/dist/misc.test.js +38 -10
- package/dist/mount-enums.js +2 -1
- package/dist/mount-input.d.ts +2 -0
- package/dist/mount-input.js +38 -22
- package/dist/mounts.test.js +81 -11
- package/dist/naming.test.d.ts +1 -0
- package/dist/naming.test.js +28 -0
- package/dist/output-reader.js +36 -4
- package/dist/package-deps.test.d.ts +1 -0
- package/dist/package-deps.test.js +49 -0
- package/dist/paths.test.js +4 -2
- package/dist/plugin-meta.test.d.ts +1 -0
- package/dist/plugin-meta.test.js +29 -0
- package/dist/project-path.js +33 -7
- package/dist/project-path.test.js +14 -0
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.js +15 -84
- package/dist/read-only-shell.js +3 -1
- package/dist/read-only-shell.test.js +80 -25
- package/dist/runtime-client.test.d.ts +1 -0
- package/dist/runtime-client.test.js +53 -0
- package/dist/secrets.test.js +21 -6
- package/dist/settings-commands.test.js +61 -14
- package/dist/settings-create.test.js +32 -10
- package/dist/settings-default-env.test.d.ts +1 -0
- package/dist/settings-default-env.test.js +175 -0
- package/dist/settings-mounts.test.js +46 -7
- package/dist/settings-schema.d.ts +6 -11
- package/dist/settings-schema.js +4 -8
- package/dist/settings-schema.test.d.ts +1 -0
- package/dist/settings-schema.test.js +35 -0
- package/dist/settings-workspaces.test.js +38 -63
- package/dist/spill-store.d.ts +1 -0
- package/dist/spill-store.js +17 -4
- package/dist/spill-store.test.js +31 -8
- package/dist/subprocess.d.ts +4 -0
- package/dist/subprocess.js +148 -162
- package/dist/subprocess.test.js +118 -8
- package/dist/terminal-preference.test.d.ts +1 -0
- package/dist/terminal-preference.test.js +62 -0
- package/dist/terminal-route.d.ts +10 -0
- package/dist/terminal-route.js +270 -0
- package/dist/terminal-route.test.d.ts +1 -0
- package/dist/terminal-route.test.js +265 -0
- package/dist/terminal-sessions.d.ts +69 -0
- package/dist/terminal-sessions.js +252 -0
- package/dist/terminal-shells.d.ts +22 -0
- package/dist/terminal-shells.js +99 -0
- package/dist/terminal-tab.test.d.ts +1 -0
- package/dist/terminal-tab.test.js +58 -0
- package/dist/terminal-targets.test.d.ts +1 -0
- package/dist/terminal-targets.test.js +43 -0
- package/dist/terminal-titles.test.d.ts +1 -0
- package/dist/terminal-titles.test.js +36 -0
- package/dist/test-support.d.ts +35 -5
- package/dist/test-support.js +91 -29
- package/dist/tool-defs.js +19 -4
- package/dist/tool-handlers.js +80 -28
- package/dist/tool-params.js +19 -5
- package/dist/version-compat.d.ts +4 -0
- package/dist/version-compat.js +29 -0
- package/dist/version-compat.test.d.ts +1 -0
- package/dist/version-compat.test.js +27 -0
- package/dist/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +19 -10
- package/dist/workspace-binding.js +252 -78
- package/dist/workspace-binding.test.js +202 -27
- package/docs/architecture.md +71 -20
- package/docs/architecture.zh.md +62 -12
- package/docs/configuration.md +61 -20
- package/docs/configuration.zh.md +53 -19
- package/docs/development-prompt.md +72 -0
- package/docs/development-prompt.zh.md +69 -0
- package/docs/development.md +184 -29
- package/docs/development.zh.md +165 -26
- package/docs/install-dsh-and-dsh-podman.md +24 -21
- package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
- package/docs/uninstall.md +83 -0
- package/docs/uninstall.zh.md +79 -0
- package/docs/update.md +120 -0
- package/docs/update.zh.md +114 -0
- package/docs/usage.md +108 -61
- package/docs/usage.zh.md +76 -38
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -25
- package/quadlet/dsh-podman-orchestrator.container +46 -0
- package/quadlet/dsh.container +35 -0
- package/LICENSE.pkg +0 -17103
- package/dist/preferences.d.ts +0 -6
- package/dist/preferences.js +0 -27
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
import { test } from "node:test";
|
|
5
5
|
import { strict as assert } from "node:assert";
|
|
6
|
-
import {
|
|
6
|
+
import { baseValue, fakeContext, fakeScope } from "./card-test-support.js";
|
|
7
7
|
import { installCardCommandDriver } from "./card-test-support.js";
|
|
8
8
|
test("create command sends PATH additions", async () => {
|
|
9
9
|
const scope = fakeScope(baseValue());
|
|
@@ -34,7 +34,7 @@ test("create command sends PATH additions", async () => {
|
|
|
34
34
|
paths: ["/opt/bin"],
|
|
35
35
|
env: {},
|
|
36
36
|
container: "",
|
|
37
|
-
|
|
37
|
+
secretEnv: {},
|
|
38
38
|
mount: null,
|
|
39
39
|
},
|
|
40
40
|
});
|
|
@@ -73,7 +73,7 @@ test("create command sends PATH additions", async () => {
|
|
|
73
73
|
mounts: [],
|
|
74
74
|
paths: ["/opt/bin", "/usr/local/bin"],
|
|
75
75
|
env: {},
|
|
76
|
-
|
|
76
|
+
secretEnv: {},
|
|
77
77
|
mount: null,
|
|
78
78
|
},
|
|
79
79
|
});
|
|
@@ -137,7 +137,14 @@ test("secret_create command drives createSecret with the name", async () => {
|
|
|
137
137
|
};
|
|
138
138
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
139
139
|
await scope.update({
|
|
140
|
-
command: {
|
|
140
|
+
command: {
|
|
141
|
+
op: "secret_create",
|
|
142
|
+
workspace: "db-pass",
|
|
143
|
+
image: "",
|
|
144
|
+
at: 6,
|
|
145
|
+
mounts: [],
|
|
146
|
+
value: "",
|
|
147
|
+
},
|
|
141
148
|
});
|
|
142
149
|
await new Promise((resolve) => setImmediate(resolve));
|
|
143
150
|
const createCall = calls.find(([method]) => method === "createSecret");
|
|
@@ -167,7 +174,13 @@ test("secret_create command drives createSecret with length when provided", asyn
|
|
|
167
174
|
};
|
|
168
175
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
169
176
|
await scope.update({
|
|
170
|
-
command: {
|
|
177
|
+
command: {
|
|
178
|
+
op: "secret_create",
|
|
179
|
+
workspace: "s",
|
|
180
|
+
image: "",
|
|
181
|
+
at: 11,
|
|
182
|
+
length: 48,
|
|
183
|
+
},
|
|
171
184
|
});
|
|
172
185
|
await new Promise((resolve) => setImmediate(resolve));
|
|
173
186
|
const createCall = calls.find(([method]) => method === "createSecret");
|
|
@@ -227,11 +240,20 @@ test("secret_create command drives createSecret with charset when provided", asy
|
|
|
227
240
|
};
|
|
228
241
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
229
242
|
await scope.update({
|
|
230
|
-
command: {
|
|
243
|
+
command: {
|
|
244
|
+
op: "secret_create",
|
|
245
|
+
workspace: "tok",
|
|
246
|
+
length: 48,
|
|
247
|
+
charset: "hex",
|
|
248
|
+
},
|
|
231
249
|
});
|
|
232
250
|
await new Promise((resolve) => setImmediate(resolve));
|
|
233
251
|
const createCall = calls.find(([method]) => method === "createSecret");
|
|
234
|
-
assert.deepEqual(createCall?.[1], {
|
|
252
|
+
assert.deepEqual(createCall?.[1], {
|
|
253
|
+
name: "tok",
|
|
254
|
+
length: 48,
|
|
255
|
+
charset: "hex",
|
|
256
|
+
});
|
|
235
257
|
assert.equal(scope.value.command, null);
|
|
236
258
|
});
|
|
237
259
|
test("recreate command drives recreateContainer with env", async () => {
|
|
@@ -297,7 +319,7 @@ test("create command with a container name routes to startContainer", async () =
|
|
|
297
319
|
at: 1,
|
|
298
320
|
mounts: [],
|
|
299
321
|
env: {},
|
|
300
|
-
|
|
322
|
+
secretEnv: {},
|
|
301
323
|
mount: null,
|
|
302
324
|
},
|
|
303
325
|
});
|
|
@@ -339,7 +361,7 @@ test("create command sends secret env", async () => {
|
|
|
339
361
|
mounts: [],
|
|
340
362
|
env: {},
|
|
341
363
|
container: "",
|
|
342
|
-
|
|
364
|
+
secretEnv: { VALKEY_PASSWORD: "valkey-tls" },
|
|
343
365
|
mount: null,
|
|
344
366
|
},
|
|
345
367
|
});
|
|
@@ -379,7 +401,7 @@ test("create command sends secret env", async () => {
|
|
|
379
401
|
mounts: [],
|
|
380
402
|
env: {},
|
|
381
403
|
container: "",
|
|
382
|
-
|
|
404
|
+
secretEnv: {},
|
|
383
405
|
mount: null,
|
|
384
406
|
},
|
|
385
407
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// The default container environment through the settings card: seeding on
|
|
5
|
+
// creation, authoritative recreates, and the synchronisation into existing
|
|
6
|
+
// containers.
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { strict as assert } from "node:assert";
|
|
9
|
+
import { baseValue, fakeContext, fakeScope, installCardCommandDriver, } from "./card-test-support.js";
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
GIT_AUTHOR_NAME: "John Doe",
|
|
12
|
+
GIT_AUTHOR_EMAIL: "john.doe@git.example",
|
|
13
|
+
};
|
|
14
|
+
function driver(containerEnv, containers = []) {
|
|
15
|
+
const scope = fakeScope(baseValue());
|
|
16
|
+
const calls = [];
|
|
17
|
+
const resolver = {
|
|
18
|
+
getConfig: () => (containerEnv === undefined ? {} : { containerEnv }),
|
|
19
|
+
setConfig: () => { },
|
|
20
|
+
async control(method, request) {
|
|
21
|
+
calls.push([method, request]);
|
|
22
|
+
if (method === "listContainers")
|
|
23
|
+
return { containers };
|
|
24
|
+
if (method === "listImages")
|
|
25
|
+
return { images: [] };
|
|
26
|
+
if (method === "listWorkspaces")
|
|
27
|
+
return { workspaces: [] };
|
|
28
|
+
return {};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return { scope, calls, resolver };
|
|
32
|
+
}
|
|
33
|
+
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
|
34
|
+
test("create seeds the default environment into a new workspace", async () => {
|
|
35
|
+
const { scope, calls, resolver } = driver(DEFAULTS);
|
|
36
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
37
|
+
await scope.update({
|
|
38
|
+
command: {
|
|
39
|
+
op: "create",
|
|
40
|
+
workspace: "w1",
|
|
41
|
+
projectName: "team",
|
|
42
|
+
image: "img1",
|
|
43
|
+
mounts: [],
|
|
44
|
+
env: { EXTRA: "1" },
|
|
45
|
+
container: "",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
await settle();
|
|
49
|
+
const create = calls.find(([method]) => method === "createWorkspace");
|
|
50
|
+
assert.deepEqual(create?.[1].env, { ...DEFAULTS, EXTRA: "1" });
|
|
51
|
+
});
|
|
52
|
+
test("create seeds the default environment into a new named container", async () => {
|
|
53
|
+
const { scope, calls, resolver } = driver(DEFAULTS);
|
|
54
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
55
|
+
await scope.update({
|
|
56
|
+
command: {
|
|
57
|
+
op: "create",
|
|
58
|
+
workspace: "w1",
|
|
59
|
+
container: "web",
|
|
60
|
+
image: "img1",
|
|
61
|
+
mounts: [],
|
|
62
|
+
env: {},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
await settle();
|
|
66
|
+
const start = calls.find(([method]) => method === "startContainer");
|
|
67
|
+
assert.deepEqual(start?.[1].env, DEFAULTS);
|
|
68
|
+
assert.equal(start?.[1].container, "web");
|
|
69
|
+
});
|
|
70
|
+
test("recreate is authoritative and does not re-seed the defaults", async () => {
|
|
71
|
+
const { scope, calls, resolver } = driver(DEFAULTS);
|
|
72
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
73
|
+
await scope.update({
|
|
74
|
+
command: {
|
|
75
|
+
op: "recreate",
|
|
76
|
+
workspace: "w1",
|
|
77
|
+
image: "",
|
|
78
|
+
env: { EXTRA: "1" },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await settle();
|
|
82
|
+
const recreate = calls.find(([method]) => method === "recreateContainer");
|
|
83
|
+
assert.deepEqual(recreate?.[1].env, { EXTRA: "1" });
|
|
84
|
+
});
|
|
85
|
+
test("default_env_sync adds only missing keys to running containers", async () => {
|
|
86
|
+
const containers = [
|
|
87
|
+
{
|
|
88
|
+
workspaceSlug: "w1",
|
|
89
|
+
containerName: "default",
|
|
90
|
+
status: "running",
|
|
91
|
+
env: { EXTRA: "1" },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
workspaceSlug: "w1",
|
|
95
|
+
containerName: "web",
|
|
96
|
+
status: "running",
|
|
97
|
+
env: { ...DEFAULTS },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
workspaceSlug: "w2",
|
|
101
|
+
containerName: "default",
|
|
102
|
+
status: "stopped",
|
|
103
|
+
env: {},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
const { scope, calls, resolver } = driver(DEFAULTS, containers);
|
|
107
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
108
|
+
await scope.update({ command: { op: "default_env_sync", workspace: "" } });
|
|
109
|
+
await settle();
|
|
110
|
+
const recreates = calls.filter(([method]) => method === "recreateContainer");
|
|
111
|
+
assert.deepEqual(recreates.map(([, request]) => request), [
|
|
112
|
+
{
|
|
113
|
+
workspaceSlug: "w1",
|
|
114
|
+
container: "default",
|
|
115
|
+
env: { ...DEFAULTS, EXTRA: "1" },
|
|
116
|
+
},
|
|
117
|
+
], "only the incomplete running container is recreated, with existing values kept");
|
|
118
|
+
assert.equal(scope.value.notice, "applied the default environment to 1 container; 1 container stopped, to pick up on their next start");
|
|
119
|
+
});
|
|
120
|
+
test("default_env_sync honors a workspace filter", async () => {
|
|
121
|
+
const containers = [
|
|
122
|
+
{
|
|
123
|
+
workspaceSlug: "w1",
|
|
124
|
+
containerName: "default",
|
|
125
|
+
status: "running",
|
|
126
|
+
env: {},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
workspaceSlug: "w2",
|
|
130
|
+
containerName: "default",
|
|
131
|
+
status: "running",
|
|
132
|
+
env: {},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
const { scope, calls, resolver } = driver(DEFAULTS, containers);
|
|
136
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
137
|
+
await scope.update({ command: { op: "default_env_sync", workspace: "w2" } });
|
|
138
|
+
await settle();
|
|
139
|
+
const recreates = calls.filter(([method]) => method === "recreateContainer");
|
|
140
|
+
assert.deepEqual(recreates.map(([, request]) => request), [
|
|
141
|
+
{ workspaceSlug: "w2", container: "default", env: DEFAULTS },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
test("default_env_sync reports nothing to do when the defaults are absent", async () => {
|
|
145
|
+
const { scope, calls, resolver } = driver(undefined, [
|
|
146
|
+
{
|
|
147
|
+
workspaceSlug: "w1",
|
|
148
|
+
containerName: "default",
|
|
149
|
+
status: "running",
|
|
150
|
+
env: {},
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
154
|
+
await calls.splice(0);
|
|
155
|
+
await scope.update({ command: { op: "default_env_sync", workspace: "" } });
|
|
156
|
+
await settle();
|
|
157
|
+
assert.equal(calls.some(([method]) => method === "recreateContainer"), false);
|
|
158
|
+
assert.equal(scope.value.notice, "");
|
|
159
|
+
});
|
|
160
|
+
test("default_env_sync reports an already-complete state", async () => {
|
|
161
|
+
const containers = [
|
|
162
|
+
{
|
|
163
|
+
workspaceSlug: "w1",
|
|
164
|
+
containerName: "default",
|
|
165
|
+
status: "running",
|
|
166
|
+
env: { ...DEFAULTS },
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const { scope, calls, resolver } = driver(DEFAULTS, containers);
|
|
170
|
+
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
171
|
+
await scope.update({ command: { op: "default_env_sync", workspace: "" } });
|
|
172
|
+
await settle();
|
|
173
|
+
assert.equal(calls.some(([method]) => method === "recreateContainer"), false);
|
|
174
|
+
assert.equal(scope.value.notice, "the default environment is already applied to every running container");
|
|
175
|
+
});
|
|
@@ -30,10 +30,17 @@ test("create command drives createWorkspace with mounts and image", async () =>
|
|
|
30
30
|
projectName: "team",
|
|
31
31
|
image: "img1",
|
|
32
32
|
at: 1,
|
|
33
|
-
mounts: [{
|
|
33
|
+
mounts: [{
|
|
34
|
+
kind: "project",
|
|
35
|
+
project: "team",
|
|
36
|
+
mode: "read_write",
|
|
37
|
+
destination: "",
|
|
38
|
+
volume: "",
|
|
39
|
+
secret: "",
|
|
40
|
+
}],
|
|
34
41
|
env: {},
|
|
35
42
|
container: "",
|
|
36
|
-
|
|
43
|
+
secretEnv: {},
|
|
37
44
|
mount: null,
|
|
38
45
|
},
|
|
39
46
|
});
|
|
@@ -43,7 +50,11 @@ test("create command drives createWorkspace with mounts and image", async () =>
|
|
|
43
50
|
workspaceSlug: "w1",
|
|
44
51
|
projectName: "team",
|
|
45
52
|
imageId: "img1",
|
|
46
|
-
mounts: [{
|
|
53
|
+
mounts: [{
|
|
54
|
+
projectName: "team",
|
|
55
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
56
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
57
|
+
}],
|
|
47
58
|
});
|
|
48
59
|
assert.equal(scope.value.command, null);
|
|
49
60
|
});
|
|
@@ -75,7 +86,7 @@ test("container_mount_add command drives addContainerMount for each kind", async
|
|
|
75
86
|
mount: {
|
|
76
87
|
kind: "project",
|
|
77
88
|
project: "team/src",
|
|
78
|
-
destination: "
|
|
89
|
+
destination: "",
|
|
79
90
|
mode: "read_only",
|
|
80
91
|
volume: "",
|
|
81
92
|
secret: "",
|
|
@@ -130,7 +141,7 @@ test("container_mount_add command drives addContainerMount for each kind", async
|
|
|
130
141
|
kind: "secret",
|
|
131
142
|
project: "",
|
|
132
143
|
destination: "/run/secrets/tls",
|
|
133
|
-
mode: "
|
|
144
|
+
mode: "read_only",
|
|
134
145
|
volume: "",
|
|
135
146
|
secret: "valkey-tls",
|
|
136
147
|
},
|
|
@@ -330,6 +341,34 @@ test("container_mount_update command drives updateContainerMount", async () => {
|
|
|
330
341
|
]);
|
|
331
342
|
assert.equal(scope.value.command, null);
|
|
332
343
|
});
|
|
344
|
+
test("container_mount_add command rejects a read-write secret mount", async () => {
|
|
345
|
+
const { scope, calls } = await installedMountScope();
|
|
346
|
+
await scope.update({
|
|
347
|
+
command: mountCommand("container_mount_add", {
|
|
348
|
+
kind: "secret",
|
|
349
|
+
secret: "valkey-tls",
|
|
350
|
+
destination: "/run/secrets/tls",
|
|
351
|
+
mode: "read_write",
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
355
|
+
assert.equal(scope.value.notice, "secret mounts are read-only; omit mode or use read_only");
|
|
356
|
+
assert.equal(calls.some(([method]) => method === "addContainerMount"), false, "a rejected secret mount must not reach the orchestrator");
|
|
357
|
+
});
|
|
358
|
+
test("container_mount_update command rejects a secret mount", async () => {
|
|
359
|
+
const { scope, calls } = await installedMountScope();
|
|
360
|
+
await scope.update({
|
|
361
|
+
command: mountCommand("container_mount_update", {
|
|
362
|
+
kind: "secret",
|
|
363
|
+
secret: "valkey-tls",
|
|
364
|
+
destination: "/run/secrets/tls",
|
|
365
|
+
mode: "read_write",
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
369
|
+
assert.equal(scope.value.notice, "secret mounts are read-only; omit mode or use read_only");
|
|
370
|
+
assert.equal(calls.some(([method]) => method === "updateContainerMount"), false, "a rejected secret mount must not reach the orchestrator");
|
|
371
|
+
});
|
|
333
372
|
test("container_mount_add command reports an unknown kind as a notice", async () => {
|
|
334
373
|
const { scope, calls } = await installedMountScope();
|
|
335
374
|
await scope.update({
|
|
@@ -389,7 +428,7 @@ test("create command reports an unknown mount kind as a notice", async () => {
|
|
|
389
428
|
}],
|
|
390
429
|
env: {},
|
|
391
430
|
container: "",
|
|
392
|
-
|
|
431
|
+
secretEnv: {},
|
|
393
432
|
mount: null,
|
|
394
433
|
},
|
|
395
434
|
});
|
|
@@ -416,7 +455,7 @@ test("create command reports an unknown mount mode as a notice", async () => {
|
|
|
416
455
|
}],
|
|
417
456
|
env: {},
|
|
418
457
|
container: "",
|
|
419
|
-
|
|
458
|
+
secretEnv: {},
|
|
420
459
|
mount: null,
|
|
421
460
|
},
|
|
422
461
|
});
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
+
import { type Volatile } from "@deepseek-ai/cordis";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
3
|
export declare const CONTAINER_NS = "podman";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
uiLocale: string;
|
|
8
|
-
}
|
|
9
|
-
export interface ContainerSettingsScope {
|
|
10
|
-
get(): ContainerSettings;
|
|
11
|
-
watch(callback: (next: ContainerSettings, prev: ContainerSettings) => void | Promise<void>): () => void;
|
|
12
|
-
update(patch: object): Promise<void>;
|
|
13
|
-
replace(section: object): Promise<void>;
|
|
4
|
+
export interface Config {
|
|
5
|
+
defaultImage: Volatile<string>;
|
|
6
|
+
uiLocale: Volatile<string>;
|
|
7
|
+
containerEnv: Volatile<Record<string, string>>;
|
|
14
8
|
}
|
|
9
|
+
export declare const Config: z<Config>;
|
package/dist/settings-schema.js
CHANGED
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
export const CONTAINER_NS = "podman";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const settingsSchema = z.object({
|
|
11
|
-
defaultImage: z.string().default(""),
|
|
12
|
-
socketsRoot: z.string().default(""),
|
|
13
|
-
uiLocale: z.string().default(""),
|
|
6
|
+
export const Config = z.object({
|
|
7
|
+
defaultImage: z.string().default("").volatile(),
|
|
8
|
+
uiLocale: z.string().default("").volatile(),
|
|
9
|
+
containerEnv: z.dict(z.string()).default({}).volatile(),
|
|
14
10
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { Config } from "./settings-schema.js";
|
|
7
|
+
import { WorkspaceResolver } from "./workspace-binding.js";
|
|
8
|
+
test("every preference is a volatile field with an empty default", () => {
|
|
9
|
+
const config = Config({});
|
|
10
|
+
const expected = [
|
|
11
|
+
["defaultImage", ""],
|
|
12
|
+
["uiLocale", ""],
|
|
13
|
+
["containerEnv", {}],
|
|
14
|
+
];
|
|
15
|
+
for (const [field, value] of expected) {
|
|
16
|
+
assert.equal(typeof config[field].get, "function", `${field} must be volatile`);
|
|
17
|
+
assert.deepEqual(config[field].get(), value, `${field} must resolve its default`);
|
|
18
|
+
}
|
|
19
|
+
// The host serves the schema to the client config form: volatility must be
|
|
20
|
+
// declared on the schema, not only in the parsed value.
|
|
21
|
+
assert.match(JSON.stringify(Config.toJSON()), /"volatile":true/);
|
|
22
|
+
});
|
|
23
|
+
test("the resolver reads its config through the live reader", () => {
|
|
24
|
+
let defaultImage = "archlinux";
|
|
25
|
+
const resolver = new WorkspaceResolver(() => ({
|
|
26
|
+
socketsRoot: "/run/dsh-podman",
|
|
27
|
+
defaultImage,
|
|
28
|
+
projectsRoot: "/projects",
|
|
29
|
+
controlToken: "token",
|
|
30
|
+
containerEnv: {},
|
|
31
|
+
}), undefined);
|
|
32
|
+
assert.equal(resolver.getConfig().defaultImage, "archlinux");
|
|
33
|
+
defaultImage = "alpine";
|
|
34
|
+
assert.equal(resolver.getConfig().defaultImage, "alpine", "a volatile change must be visible without rebuilding the resolver");
|
|
35
|
+
});
|
|
@@ -18,6 +18,8 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
18
18
|
setConfig: () => { },
|
|
19
19
|
async control(method, request) {
|
|
20
20
|
calls.push([method, request]);
|
|
21
|
+
if (method === "getVersion")
|
|
22
|
+
return { version: "0.2.0", commit: "abc" };
|
|
21
23
|
if (method === "listContainers") {
|
|
22
24
|
return { containers: [{ containerName: "c1", workspaceSlug: "w1" }] };
|
|
23
25
|
}
|
|
@@ -25,7 +27,12 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
25
27
|
return { images: [{ imageId: "img1" }] };
|
|
26
28
|
}
|
|
27
29
|
if (method === "listWorkspaces") {
|
|
28
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
workspaces: [{
|
|
32
|
+
workspaceSlug: "w1",
|
|
33
|
+
mounts: [{ projectName: "team/app" }],
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
29
36
|
}
|
|
30
37
|
if (method === "listVolumes") {
|
|
31
38
|
return { volumes: [{ name: "data" }, { name: "cache" }] };
|
|
@@ -38,7 +45,15 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
38
45
|
};
|
|
39
46
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
40
47
|
await scope.update({}); // settle the queued async refresh
|
|
41
|
-
assert.deepEqual(calls.map(([method]) => method), [
|
|
48
|
+
assert.deepEqual(calls.map(([method]) => method), [
|
|
49
|
+
"getVersion",
|
|
50
|
+
"listContainers",
|
|
51
|
+
"listImages",
|
|
52
|
+
"listWorkspaces",
|
|
53
|
+
"listVolumes",
|
|
54
|
+
"listSecrets",
|
|
55
|
+
"listCaches",
|
|
56
|
+
]);
|
|
42
57
|
assert.equal(scope.value.containers.length, 1);
|
|
43
58
|
assert.equal(scope.value.images.length, 1);
|
|
44
59
|
const workspaces = scope.value.workspaces;
|
|
@@ -46,62 +61,9 @@ test("refresh on install publishes containers, images and workspaces", async ()
|
|
|
46
61
|
assert.equal(workspaces[0].workspaceSlug, "w1");
|
|
47
62
|
assert.equal(workspaces[0].projectName, "team/app");
|
|
48
63
|
assert.deepEqual(scope.value.volumes, [{ name: "data" }, { name: "cache" }]);
|
|
49
|
-
assert.deepEqual(scope.value.secrets, [{ name: "db-pass" }, {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const scope = fakeScope(baseValue());
|
|
53
|
-
const setConfigCalls = [];
|
|
54
|
-
const resolver = {
|
|
55
|
-
getConfig: () => ({}),
|
|
56
|
-
setConfig: (patch) => setConfigCalls.push(patch),
|
|
57
|
-
async control(method) {
|
|
58
|
-
if (method === "listContainers")
|
|
59
|
-
return { containers: [] };
|
|
60
|
-
if (method === "listImages")
|
|
61
|
-
return { images: [] };
|
|
62
|
-
if (method === "listWorkspaces")
|
|
63
|
-
return { workspaces: [] };
|
|
64
|
-
return {};
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
68
|
-
await scope.update({});
|
|
69
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
70
|
-
assert.ok(setConfigCalls.length > 0, "handle must write resolver config");
|
|
71
|
-
for (const patch of setConfigCalls) {
|
|
72
|
-
assert.ok(!Object.prototype.hasOwnProperty.call(patch, "projectsRoot"), "projectsRoot must not be written via setConfig");
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
test("settings seed the image and sockets root from the resolver config", async () => {
|
|
76
|
-
let registeredBase;
|
|
77
|
-
const scope = fakeScope(baseValue());
|
|
78
|
-
const context = {
|
|
79
|
-
inject(deps, callback) {
|
|
80
|
-
assert.deepEqual(deps, ["settings"]);
|
|
81
|
-
callback({
|
|
82
|
-
settings: {
|
|
83
|
-
register(_namespace, _schema, options) {
|
|
84
|
-
registeredBase = options?.base ?? {};
|
|
85
|
-
return scope;
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
const resolver = {
|
|
92
|
-
getConfig: () => ({
|
|
93
|
-
defaultImage: "archlinux",
|
|
94
|
-
socketsRoot: "/run/dsh-podman",
|
|
95
|
-
projectsRoot: "/projects",
|
|
96
|
-
}),
|
|
97
|
-
setConfig: () => { },
|
|
98
|
-
async control() {
|
|
99
|
-
return {};
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
await installCardCommandDriver(context, resolver);
|
|
103
|
-
assert.equal(registeredBase?.defaultImage, "archlinux");
|
|
104
|
-
assert.equal(registeredBase?.socketsRoot, "/run/dsh-podman");
|
|
64
|
+
assert.deepEqual(scope.value.secrets, [{ name: "db-pass" }, {
|
|
65
|
+
name: "api-key",
|
|
66
|
+
}]);
|
|
105
67
|
});
|
|
106
68
|
test("create command drives createWorkspace with env", async () => {
|
|
107
69
|
const scope = fakeScope(baseValue());
|
|
@@ -131,7 +93,7 @@ test("create command drives createWorkspace with env", async () => {
|
|
|
131
93
|
mounts: [],
|
|
132
94
|
env: { A: "1", B: "2" },
|
|
133
95
|
container: "",
|
|
134
|
-
|
|
96
|
+
secretEnv: {},
|
|
135
97
|
mount: null,
|
|
136
98
|
},
|
|
137
99
|
});
|
|
@@ -173,7 +135,7 @@ test("create command drives createWorkspace without env when empty", async () =>
|
|
|
173
135
|
mounts: [],
|
|
174
136
|
env: {},
|
|
175
137
|
container: "",
|
|
176
|
-
|
|
138
|
+
secretEnv: {},
|
|
177
139
|
mount: null,
|
|
178
140
|
},
|
|
179
141
|
});
|
|
@@ -190,7 +152,12 @@ test("workspace list comes from the dsh registry even without orchestrator state
|
|
|
190
152
|
const scope = fakeScope(baseValue());
|
|
191
153
|
const registry = {
|
|
192
154
|
list: () => [
|
|
193
|
-
{
|
|
155
|
+
{
|
|
156
|
+
id: WORKSPACE_ID,
|
|
157
|
+
path: "/projects/team/app",
|
|
158
|
+
title: "app",
|
|
159
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
160
|
+
},
|
|
194
161
|
],
|
|
195
162
|
};
|
|
196
163
|
const resolver = {
|
|
@@ -225,7 +192,12 @@ test("dsh workspace layers orchestrator container info", async () => {
|
|
|
225
192
|
const scope = fakeScope(baseValue());
|
|
226
193
|
const registry = {
|
|
227
194
|
list: () => [
|
|
228
|
-
{
|
|
195
|
+
{
|
|
196
|
+
id: WORKSPACE_ID,
|
|
197
|
+
path: "/projects/team/app",
|
|
198
|
+
title: "app",
|
|
199
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
200
|
+
},
|
|
229
201
|
],
|
|
230
202
|
};
|
|
231
203
|
const resolver = {
|
|
@@ -247,7 +219,10 @@ test("dsh workspace layers orchestrator container info", async () => {
|
|
|
247
219
|
containerName: `dsh-podman-${WORKSPACE_ID}-default`,
|
|
248
220
|
imageId: "arch",
|
|
249
221
|
status: "running",
|
|
250
|
-
mounts: [{
|
|
222
|
+
mounts: [{
|
|
223
|
+
projectName: "team/app",
|
|
224
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
225
|
+
}],
|
|
251
226
|
}],
|
|
252
227
|
};
|
|
253
228
|
}
|
package/dist/spill-store.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface SpillRef {
|
|
|
12
12
|
bytes: number;
|
|
13
13
|
retrievalHint: string;
|
|
14
14
|
}
|
|
15
|
+
export declare function sessionSegment(sessionId: unknown): string;
|
|
15
16
|
export declare function createSpillStore(ctx: any, resolver: WorkspaceResolver): {
|
|
16
17
|
saveText(input: SpillRequest): Promise<SpillRef>;
|
|
17
18
|
};
|
package/dist/spill-store.js
CHANGED
|
@@ -17,10 +17,23 @@ function spillName(suggestedName) {
|
|
|
17
17
|
.slice(0, 64);
|
|
18
18
|
return `${randomUUID().replace(/-/g, "")}-${safe === "" ? "spill.txt" : safe}`;
|
|
19
19
|
}
|
|
20
|
-
// sessionSegment is the per-session directory name
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// sessionSegment is the per-session directory name. Session ids are not
|
|
21
|
+
// guaranteed to be UUIDs (dsh mints `session-<n>`, `<id>-session-<uuid>`, and
|
|
22
|
+
// `webhook-<uuid>`, and accepts a caller-supplied id), so the id is encoded
|
|
23
|
+
// rather than trusted: a short readable prefix from a safe alphabet plus the
|
|
24
|
+
// full id in hex — deterministic, collision-free, and never "." or "..".
|
|
25
|
+
export function sessionSegment(sessionId) {
|
|
26
|
+
const id = String(sessionId ?? "");
|
|
27
|
+
const encoded = Buffer.from(id, "utf8").toString("hex");
|
|
28
|
+
const readable = id
|
|
29
|
+
.replace(/[^A-Za-z0-9_-]/g, "_")
|
|
30
|
+
.replace(/_{2,}/g, "_")
|
|
31
|
+
.replace(/^[_-]+/, "")
|
|
32
|
+
.slice(0, 24)
|
|
33
|
+
.replace(/[_-]+$/, "");
|
|
34
|
+
if (readable !== "")
|
|
35
|
+
return `${readable}-${encoded}`;
|
|
36
|
+
return encoded === "" ? "session" : encoded;
|
|
24
37
|
}
|
|
25
38
|
// createSpillStore backs `ctx.spillStore` with the session's container:
|
|
26
39
|
// oversized tool results are written inside the guest, and the locator is the
|