@exagone313/dsh-podman 0.2.0-rc.4 → 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 +12 -0
- 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 +103 -35
- package/dist/card-route.test.js +60 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +76 -70
- package/dist/client/card-protocol.d.ts +3 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +13 -18
- package/dist/client/container-card-controller.js +86 -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 +13109 -2117
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +88 -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.js +2 -1
- 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 +8 -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 +36 -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/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +18 -9
- package/dist/workspace-binding.js +244 -77
- package/dist/workspace-binding.test.js +189 -25
- package/docs/architecture.md +62 -21
- package/docs/architecture.zh.md +57 -13
- 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 +141 -21
- package/docs/development.zh.md +125 -17
- package/docs/install-dsh-and-dsh-podman.md +21 -8
- package/docs/install-dsh-and-dsh-podman.zh.md +18 -8
- 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
|
@@ -31,6 +31,20 @@ test("confineToRoot clamps a directory to the projects root", () => {
|
|
|
31
31
|
assert.equal(confineToRoot("/projects", ""), "/projects");
|
|
32
32
|
assert.equal(confineToRoot("/projects", "/projects/web/"), "/projects/web", "trailing slashes are ignored");
|
|
33
33
|
});
|
|
34
|
+
test("project paths resolve their dots and never escape the root", () => {
|
|
35
|
+
// A ".." that stays inside the root is resolved.
|
|
36
|
+
assert.equal(confineToRoot("/projects", "/projects/a/../b"), "/projects/b");
|
|
37
|
+
assert.equal(hostPathForProjectName("/projects", "a/../b"), "/projects/b");
|
|
38
|
+
// A ".." that would climb above the root clamps to the root, on both the
|
|
39
|
+
// path-building and the path-clamping helper.
|
|
40
|
+
assert.equal(confineToRoot("/projects", "/projects/../etc"), "/projects");
|
|
41
|
+
assert.equal(confineToRoot("/projects", "/projects/a/../../etc"), "/projects");
|
|
42
|
+
assert.equal(hostPathForProjectName("/projects", "../etc"), "/projects");
|
|
43
|
+
assert.equal(hostPathForProjectName("/projects", "a/../../etc"), "/projects");
|
|
44
|
+
assert.equal(hostPathForProjectName("/projects", "/../etc"), "/projects");
|
|
45
|
+
// A "." segment is dropped, not kept as a directory name.
|
|
46
|
+
assert.equal(hostPathForProjectName("/projects", "./web"), "/projects/web");
|
|
47
|
+
});
|
|
34
48
|
test("parentDirectory never climbs above the projects root", () => {
|
|
35
49
|
assert.equal(parentDirectory("/projects", "/projects/web/sub"), "/projects/web");
|
|
36
50
|
assert.equal(parentDirectory("/projects", "/projects/web"), "/projects");
|
package/dist/prompts.d.ts
CHANGED
|
@@ -7,7 +7,3 @@ export declare function podmanRuntimeSection(ctx: any): {
|
|
|
7
7
|
scope?: unknown;
|
|
8
8
|
}) => string;
|
|
9
9
|
};
|
|
10
|
-
export declare const PODMAN_OPS_PRESET_YML = "name: Podman operator mode\ndescription: Podman-focused container operations \u2014 manage images, containers, volumes, mounts, secrets, and daemons. Inspections and lifecycle run directly; commands, container file edits, daemon starts, and secret changes ask for approval; supports web research.\norder: 2\n";
|
|
11
|
-
export declare const PODMAN_OPS_AGENT_CORDIS_YML = "- id: persona\n name: '@deepseek-ai/dsh-persona'\n config:\n suffix: Your working directory is {{cwd}}.\n prefix: >-\n You are a Podman container-operations agent powered by the {{model}} model.\n\n You manage container infrastructure through the dsh-podman tools: images\n (image_list, image_get, image_build, image_rebuild, image_rebuild_all,\n image_remove), containers (container_list, container_start,\n container_recreate, container_remove), mounts (container_mount_list,\n container_mount_add, container_mount_remove, container_mount_update),\n PATH additions (container_path_set, container_path_add,\n container_path_remove), volumes (volume_list,\n volume_create, volume_remove), secrets (secret_list, secret_create,\n secret_remove, container_secret_add, container_secret_remove), daemons\n (daemon_list, daemon_start, daemon_stop, daemon_restart, daemon_logs),\n and container inspection (container_read, container_glob,\n container_grep).\n\n Inspection and lifecycle operations run directly. Running commands inside\n a container (container_bash, container_exec), editing container files\n (container_write, container_edit), starting daemons (daemon_start),\n exposing or removing secret environment variables (container_secret_add,\n container_secret_remove), removing secrets (secret_remove), and\n rebuilding all images (image_rebuild_all) require the user's approval.\n Use web_search to research images and documentation. Use\n ask_user_question for user decisions and todo_write to track work.\n- id: tool-ask-user\n name: '@deepseek-ai/dsh-tool-ask-user'\n- id: tool-todo\n name: '@deepseek-ai/dsh-tool-todo'\n config:\n allowParallelInProgress: true\n- id: tool-web\n name: '@deepseek-ai/dsh-tool-web'\n config:\n fetch: false\n searchTimeoutMs: 60000\n";
|
|
12
|
-
export declare function ensurePodmanOpsPreset(ctx: any, dir?: string): void;
|
|
13
|
-
export declare function homePresetsRoot(): string;
|
package/dist/prompts.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
|
-
import { PODMAN_OPS_PRESET } from "./approval.js";
|
|
5
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
4
|
// Prompt section the harness registers to name its own on-disk checkout.
|
|
9
5
|
// Mirrors @deepseek-ai/dsh-app-boot's HARNESS_SOURCE_SECTION.
|
|
10
6
|
export const HARNESS_SOURCE_SECTION = "harness:source";
|
|
@@ -50,6 +46,19 @@ const RUNTIME_CONTAINER_ONLY = "This dsh session's container tools run in a Podm
|
|
|
50
46
|
"not exist. Containers share the host kernel, so `uname -a`, `uname -r`, " +
|
|
51
47
|
"and `/proc/version` do report the host kernel; the container's own identity " +
|
|
52
48
|
"shows in `hostname`, `/etc/os-release`, and `/proc/1/cmdline`.";
|
|
49
|
+
// Storage guidance both runtime wordings carry. It names only tools that exist
|
|
50
|
+
// in every agent composition (`image_build`, `container_start`,
|
|
51
|
+
// `container_recreate`, volume mounts), so it is safe to append verbatim to the
|
|
52
|
+
// wording for an agent with or without the built-in tools.
|
|
53
|
+
const RUNTIME_STORAGE_NOTES = " To install software, prefer building a custom image with `image_build` (a " +
|
|
54
|
+
"short name, a base or custom parent image, and the packages to add) and " +
|
|
55
|
+
"then running containers from it with `container_start` or " +
|
|
56
|
+
"`container_recreate` (`image`); an install into a running container does " +
|
|
57
|
+
"not survive a recreate, while an image change does. For data that must " +
|
|
58
|
+
"outlive a recreate, mount a named `volume` (auto-created on first use) " +
|
|
59
|
+
"instead of a `tmpfs`, including the container's `/tmp` — a tool call that " +
|
|
60
|
+
"recreates the container (a mount or secret change, `container_recreate`, a " +
|
|
61
|
+
"read-only remount) clears tmpfs contents.";
|
|
53
62
|
// Correct the model's host/container mental model: the built-in shell and
|
|
54
63
|
// filesystem tools are container-backed too, so there is no host shell. It sits
|
|
55
64
|
// just before the tool sections so the correction lands next to the tool
|
|
@@ -64,86 +73,8 @@ export function podmanRuntimeSection(ctx) {
|
|
|
64
73
|
return {
|
|
65
74
|
name: "podman:runtime",
|
|
66
75
|
order: 950,
|
|
67
|
-
text: ({ scope } = {}) => BUILTIN_TOOLS.some((name) => ctx.tools.get(name, scope) !== undefined)
|
|
76
|
+
text: ({ scope } = {}) => (BUILTIN_TOOLS.some((name) => ctx.tools.get(name, scope) !== undefined)
|
|
68
77
|
? RUNTIME_WITH_BUILTINS
|
|
69
|
-
: RUNTIME_CONTAINER_ONLY,
|
|
78
|
+
: RUNTIME_CONTAINER_ONLY) + RUNTIME_STORAGE_NOTES,
|
|
70
79
|
};
|
|
71
80
|
}
|
|
72
|
-
// The "Podman operator mode" preset metadata, shipped verbatim into the
|
|
73
|
-
// harness's user-presets root.
|
|
74
|
-
export const PODMAN_OPS_PRESET_YML = `name: Podman operator mode
|
|
75
|
-
description: Podman-focused container operations — manage images, containers, volumes, mounts, secrets, and daemons. Inspections and lifecycle run directly; commands, container file edits, daemon starts, and secret changes ask for approval; supports web research.
|
|
76
|
-
order: 2
|
|
77
|
-
`;
|
|
78
|
-
// The "Podman operator mode" agent composition: a Podman-focused persona plus
|
|
79
|
-
// the built-in task and web tools. The plugin's own container tools are global
|
|
80
|
-
// and need no rows; the command/file/daemon/secret tools stay available but
|
|
81
|
-
// gated by the pre-execute policy (PODMAN_OPS_APPROVAL_TOOLS + global flags).
|
|
82
|
-
export const PODMAN_OPS_AGENT_CORDIS_YML = `- id: persona
|
|
83
|
-
name: '@deepseek-ai/dsh-persona'
|
|
84
|
-
config:
|
|
85
|
-
suffix: Your working directory is {{cwd}}.
|
|
86
|
-
prefix: >-
|
|
87
|
-
You are a Podman container-operations agent powered by the {{model}} model.
|
|
88
|
-
|
|
89
|
-
You manage container infrastructure through the dsh-podman tools: images
|
|
90
|
-
(image_list, image_get, image_build, image_rebuild, image_rebuild_all,
|
|
91
|
-
image_remove), containers (container_list, container_start,
|
|
92
|
-
container_recreate, container_remove), mounts (container_mount_list,
|
|
93
|
-
container_mount_add, container_mount_remove, container_mount_update),
|
|
94
|
-
PATH additions (container_path_set, container_path_add,
|
|
95
|
-
container_path_remove), volumes (volume_list,
|
|
96
|
-
volume_create, volume_remove), secrets (secret_list, secret_create,
|
|
97
|
-
secret_remove, container_secret_add, container_secret_remove), daemons
|
|
98
|
-
(daemon_list, daemon_start, daemon_stop, daemon_restart, daemon_logs),
|
|
99
|
-
and container inspection (container_read, container_glob,
|
|
100
|
-
container_grep).
|
|
101
|
-
|
|
102
|
-
Inspection and lifecycle operations run directly. Running commands inside
|
|
103
|
-
a container (container_bash, container_exec), editing container files
|
|
104
|
-
(container_write, container_edit), starting daemons (daemon_start),
|
|
105
|
-
exposing or removing secret environment variables (container_secret_add,
|
|
106
|
-
container_secret_remove), removing secrets (secret_remove), and
|
|
107
|
-
rebuilding all images (image_rebuild_all) require the user's approval.
|
|
108
|
-
Use web_search to research images and documentation. Use
|
|
109
|
-
ask_user_question for user decisions and todo_write to track work.
|
|
110
|
-
- id: tool-ask-user
|
|
111
|
-
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
112
|
-
- id: tool-todo
|
|
113
|
-
name: '@deepseek-ai/dsh-tool-todo'
|
|
114
|
-
config:
|
|
115
|
-
allowParallelInProgress: true
|
|
116
|
-
- id: tool-web
|
|
117
|
-
name: '@deepseek-ai/dsh-tool-web'
|
|
118
|
-
config:
|
|
119
|
-
fetch: false
|
|
120
|
-
searchTimeoutMs: 60000
|
|
121
|
-
`;
|
|
122
|
-
// Install the "Podman operator mode" agent preset into the harness's
|
|
123
|
-
// user-presets root (~/.dsh/.agent-presets/<id>). The plugin owns the preset
|
|
124
|
-
// content and (re)writes it on every load, so a local copy is always brought
|
|
125
|
-
// back to the shipped composition. Best-effort — a failure only logs.
|
|
126
|
-
export function ensurePodmanOpsPreset(ctx, dir) {
|
|
127
|
-
const log = (message, ...args) => {
|
|
128
|
-
try {
|
|
129
|
-
ctx?.logger?.warn?.(message, ...args);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Logger failure must not abort preset installation.
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
try {
|
|
136
|
-
const target = dir ?? join(homePresetsRoot(), PODMAN_OPS_PRESET);
|
|
137
|
-
mkdirSync(target, { recursive: true });
|
|
138
|
-
writeFileSync(join(target, "preset.yml"), PODMAN_OPS_PRESET_YML);
|
|
139
|
-
writeFileSync(join(target, "agent.cordis.yml"), PODMAN_OPS_AGENT_CORDIS_YML);
|
|
140
|
-
log(`[dsh-podman] wrote the "${PODMAN_OPS_PRESET}" agent preset to %s`, target);
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
log(`[dsh-podman] could not install the "${PODMAN_OPS_PRESET}" agent preset: %o`, error);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// The harness user-presets root ($DSH_HOME or ~/.dsh, plus .agent-presets).
|
|
147
|
-
export function homePresetsRoot() {
|
|
148
|
-
return join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), ".agent-presets");
|
|
149
|
-
}
|
package/dist/read-only-shell.js
CHANGED
|
@@ -65,7 +65,9 @@ export function createReadOnlyShellGate(deps) {
|
|
|
65
65
|
// plugin's tools, the default container for the built-in ones.
|
|
66
66
|
function containerOf(exec) {
|
|
67
67
|
const container = exec?.arguments?.container;
|
|
68
|
-
return typeof container === "string" && container !== ""
|
|
68
|
+
return typeof container === "string" && container !== ""
|
|
69
|
+
? container
|
|
70
|
+
: "default";
|
|
69
71
|
}
|
|
70
72
|
// Whether a mount kind carries a mode: project and volume mounts do; tmpfs is
|
|
71
73
|
// always read-write and secret mounts take none.
|
|
@@ -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 { READ_ONLY_GATED_TOOLS, REMOUNT_TOOL_NAME,
|
|
6
|
+
import { createReadOnlyShellGate, READ_ONLY_GATED_TOOLS, REMOUNT_TOOL_NAME, } from "./index.js";
|
|
7
7
|
import { WORKSPACE_ID } from "./test-support.js";
|
|
8
8
|
const EXEC = {
|
|
9
9
|
name: "bash",
|
|
@@ -22,17 +22,15 @@ function gateFixture(mounts, options = {}) {
|
|
|
22
22
|
async control(method, request) {
|
|
23
23
|
controlCalls.push([method, request]);
|
|
24
24
|
if (method === "listContainers") {
|
|
25
|
-
return mounts === undefined
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
],
|
|
35
|
-
};
|
|
25
|
+
return mounts === undefined ? { containers: [] } : {
|
|
26
|
+
containers: [
|
|
27
|
+
{
|
|
28
|
+
workspaceSlug: WORKSPACE_ID,
|
|
29
|
+
containerName,
|
|
30
|
+
mounts,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
36
34
|
}
|
|
37
35
|
return {};
|
|
38
36
|
},
|
|
@@ -68,9 +66,21 @@ test("the gate covers the shell, file, and daemon tools", () => {
|
|
|
68
66
|
});
|
|
69
67
|
test("all mounts read-only allows the tool without a prompt", async () => {
|
|
70
68
|
const { gate, controlCalls, approvals } = gateFixture([
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
{
|
|
70
|
+
projectName: "team",
|
|
71
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
72
|
+
mode: "MOUNT_MODE_READ_ONLY",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
kind: "MOUNT_KIND_TMPFS",
|
|
76
|
+
destination: "/scratch",
|
|
77
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: "MOUNT_KIND_SECRET",
|
|
81
|
+
secret: "tls",
|
|
82
|
+
destination: "/run/secrets/tls",
|
|
83
|
+
},
|
|
74
84
|
]);
|
|
75
85
|
assert.deepEqual(await gate(EXEC, "en"), { kind: "allow" });
|
|
76
86
|
assert.deepEqual(approvals, []);
|
|
@@ -78,10 +88,27 @@ test("all mounts read-only allows the tool without a prompt", async () => {
|
|
|
78
88
|
});
|
|
79
89
|
test("a read-write mount prompts and then remounts read-only", async () => {
|
|
80
90
|
const { gate, controlCalls, approvals } = gateFixture([
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
{
|
|
92
|
+
projectName: "team",
|
|
93
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
94
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
kind: "MOUNT_KIND_VOLUME",
|
|
98
|
+
volume: "data",
|
|
99
|
+
destination: "/data",
|
|
100
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
kind: "MOUNT_KIND_TMPFS",
|
|
104
|
+
destination: "/scratch",
|
|
105
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: "MOUNT_KIND_SECRET",
|
|
109
|
+
secret: "tls",
|
|
110
|
+
destination: "/run/secrets/tls",
|
|
111
|
+
},
|
|
85
112
|
]);
|
|
86
113
|
assert.deepEqual(await gate(EXEC, "en"), { kind: "allow" });
|
|
87
114
|
assert.equal(approvals.length, 1);
|
|
@@ -95,15 +122,39 @@ test("a read-write mount prompts and then remounts read-only", async () => {
|
|
|
95
122
|
workspaceSlug: WORKSPACE_ID,
|
|
96
123
|
container: "default",
|
|
97
124
|
mounts: [
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
{
|
|
126
|
+
projectName: "team",
|
|
127
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
128
|
+
mode: "MOUNT_MODE_READ_ONLY",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
projectName: "",
|
|
132
|
+
kind: "MOUNT_KIND_VOLUME",
|
|
133
|
+
mode: "MOUNT_MODE_READ_ONLY",
|
|
134
|
+
destination: "/data",
|
|
135
|
+
volume: "data",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
projectName: "",
|
|
139
|
+
kind: "MOUNT_KIND_TMPFS",
|
|
140
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
141
|
+
destination: "/scratch",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
projectName: "",
|
|
145
|
+
kind: "MOUNT_KIND_SECRET",
|
|
146
|
+
destination: "/run/secrets/tls",
|
|
147
|
+
secret: "tls",
|
|
148
|
+
},
|
|
102
149
|
],
|
|
103
150
|
});
|
|
104
151
|
});
|
|
105
152
|
test("a declined prompt denies without remounting", async () => {
|
|
106
|
-
const { gate, controlCalls, approvals } = gateFixture([{
|
|
153
|
+
const { gate, controlCalls, approvals } = gateFixture([{
|
|
154
|
+
projectName: "team",
|
|
155
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
156
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
157
|
+
}], { approve: false });
|
|
107
158
|
const decision = (await gate(EXEC, "en"));
|
|
108
159
|
assert.equal(decision.kind, "deny");
|
|
109
160
|
assert.match(decision.reason, /mounts were not remounted/);
|
|
@@ -116,7 +167,11 @@ test("an unreadable container falls through to the plain denial", async () => {
|
|
|
116
167
|
assert.deepEqual(approvals, []);
|
|
117
168
|
});
|
|
118
169
|
test("a plugin tool targets its own container", async () => {
|
|
119
|
-
const { gate, controlCalls } = gateFixture([{
|
|
170
|
+
const { gate, controlCalls } = gateFixture([{
|
|
171
|
+
projectName: "team",
|
|
172
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
173
|
+
mode: "MOUNT_MODE_READ_ONLY",
|
|
174
|
+
}], { containerName: "web" });
|
|
120
175
|
const exec = {
|
|
121
176
|
name: "container_bash",
|
|
122
177
|
arguments: { container: "web" },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { abortError, closeClients, controlClient, guestClient, unary, } from "./grpc/runtime-client.js";
|
|
7
|
+
test("clients are reused per socket and a close drops the cache", () => {
|
|
8
|
+
const first = controlClient("/run/dsh-podman/orchestrator.sock");
|
|
9
|
+
const second = controlClient("/run/dsh-podman/orchestrator.sock");
|
|
10
|
+
assert.equal(second, first, "the same socket must reuse one channel");
|
|
11
|
+
assert.notEqual(controlClient("/tmp/other.sock"), first, "a different socket gets its own channel");
|
|
12
|
+
const guest = guestClient("/run/dsh-podman/w/guest.sock");
|
|
13
|
+
closeClients();
|
|
14
|
+
assert.notEqual(controlClient("/run/dsh-podman/orchestrator.sock"), first, "a disposed client is not served from the cache");
|
|
15
|
+
assert.notEqual(guestClient("/run/dsh-podman/w/guest.sock"), guest);
|
|
16
|
+
closeClients();
|
|
17
|
+
});
|
|
18
|
+
test("unary rejects a pre-aborted call without invoking the client", async () => {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
controller.abort();
|
|
21
|
+
let called = false;
|
|
22
|
+
const client = {
|
|
23
|
+
method: () => {
|
|
24
|
+
called = true;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
await assert.rejects(() => unary(client, "method", {}, undefined, controller.signal), (error) => error.code === "ABORTED");
|
|
28
|
+
assert.equal(called, false);
|
|
29
|
+
});
|
|
30
|
+
test("unary cancels a pending call when the signal aborts", async () => {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
let settle;
|
|
34
|
+
const client = {
|
|
35
|
+
method: (_request, _metadata, callback) => {
|
|
36
|
+
settle = callback;
|
|
37
|
+
return {
|
|
38
|
+
cancel: () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
// grpc-js settles a cancelled call with a CANCELLED status.
|
|
41
|
+
settle?.(Object.assign(new Error("cancelled"), { code: 1 }));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const pending = unary(client, "method", {}, undefined, controller.signal);
|
|
47
|
+
controller.abort();
|
|
48
|
+
await assert.rejects(pending, (error) => error.code === "ABORTED");
|
|
49
|
+
assert.equal(cancelled, true);
|
|
50
|
+
});
|
|
51
|
+
test("abortError carries a stable code", () => {
|
|
52
|
+
assert.equal(abortError("exec").code, "ABORTED");
|
|
53
|
+
});
|
package/dist/secrets.test.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { MOUNT_EXEC, SECRET_TOOLS, WORKSPACE_ID,
|
|
7
|
-
import { READ_ONLY_TOOLS,
|
|
6
|
+
import { MOUNT_EXEC, mountRequestRecorder, SECRET_TOOLS, WORKSPACE_ID, } from "./test-support.js";
|
|
7
|
+
import { READ_ONLY_TOOLS, toolHandlers, TOOLS } from "./index.js";
|
|
8
8
|
test("secret tools are registered with the expected schemas", () => {
|
|
9
9
|
assert.ok(READ_ONLY_TOOLS.has("secret_list"), "secret_list must be read-only");
|
|
10
10
|
const listTool = TOOLS.find((entry) => entry.name === "secret_list");
|
|
@@ -29,7 +29,11 @@ test("secret tools are registered with the expected schemas", () => {
|
|
|
29
29
|
const addTool = TOOLS.find((entry) => entry.name === "container_secret_add");
|
|
30
30
|
assert.ok(addTool, "container_secret_add registered");
|
|
31
31
|
assert.equal(addTool.approval, true, "container_secret_add must require approval");
|
|
32
|
-
assert.deepEqual(addTool.parameters.required, [
|
|
32
|
+
assert.deepEqual(addTool.parameters.required, [
|
|
33
|
+
"container",
|
|
34
|
+
"env",
|
|
35
|
+
"secret",
|
|
36
|
+
]);
|
|
33
37
|
assert.equal(addTool.parameters.properties.secret.type, "string");
|
|
34
38
|
assert.equal(addTool.parameters.properties.env.type, "string");
|
|
35
39
|
const removeEnvTool = TOOLS.find((entry) => entry.name === "container_secret_remove");
|
|
@@ -86,15 +90,26 @@ test("secret mount kinds forward the secret to the orchestrator", async () => {
|
|
|
86
90
|
});
|
|
87
91
|
test("mount tools reject a read-write secret mount", async () => {
|
|
88
92
|
const { requests, resolver } = mountRequestRecorder();
|
|
89
|
-
await assert.rejects(() => toolHandlers.container_mount_add(resolver, {
|
|
93
|
+
await assert.rejects(() => toolHandlers.container_mount_add(resolver, {
|
|
94
|
+
container: "web",
|
|
95
|
+
kind: "secret",
|
|
96
|
+
secret: "tls",
|
|
97
|
+
destination: "/run/secrets/tls",
|
|
98
|
+
mode: "read_write",
|
|
99
|
+
}, MOUNT_EXEC), /secret mounts are read-only/);
|
|
90
100
|
await assert.rejects(() => toolHandlers.container_start(resolver, {
|
|
91
101
|
container: "web",
|
|
92
102
|
image: "img1",
|
|
93
|
-
mounts: [{
|
|
103
|
+
mounts: [{
|
|
104
|
+
kind: "secret",
|
|
105
|
+
secret: "tls",
|
|
106
|
+
destination: "/run/secrets/tls",
|
|
107
|
+
mode: "read_write",
|
|
108
|
+
}],
|
|
94
109
|
}, MOUNT_EXEC), /secret mounts are read-only/);
|
|
95
110
|
assert.deepEqual(requests, [], "a rejected mount must not reach the orchestrator");
|
|
96
111
|
});
|
|
97
|
-
test("container_list returns
|
|
112
|
+
test("container_list returns secretEnv maps on container rows", async () => {
|
|
98
113
|
const resolver = {
|
|
99
114
|
registry: {
|
|
100
115
|
resolveByPath: async () => ({ id: WORKSPACE_ID }),
|
|
@@ -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, installedMountScope, mountCommand, } from "./card-test-support.js";
|
|
7
7
|
import { installCardCommandDriver } from "./card-test-support.js";
|
|
8
8
|
test("remove command drives removeContainer and clears the command", async () => {
|
|
9
9
|
const scope = fakeScope(baseValue());
|
|
@@ -65,27 +65,50 @@ test("cache_clean command drives cleanCaches, maps the mode, and reports the res
|
|
|
65
65
|
return { containers: [] };
|
|
66
66
|
if (method === "listImages")
|
|
67
67
|
return { images: [] };
|
|
68
|
-
if (method === "cleanCaches")
|
|
68
|
+
if (method === "cleanCaches") {
|
|
69
69
|
return { removedFiles: 2, removedBytes: "2048" };
|
|
70
|
+
}
|
|
70
71
|
if (method === "listCaches") {
|
|
71
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
caches: [{
|
|
74
|
+
manager: "pacman",
|
|
75
|
+
path: "/cache",
|
|
76
|
+
files: 1,
|
|
77
|
+
bytes: "1024",
|
|
78
|
+
}],
|
|
79
|
+
};
|
|
72
80
|
}
|
|
73
81
|
return {};
|
|
74
82
|
},
|
|
75
83
|
};
|
|
76
84
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
77
85
|
await scope.update({
|
|
78
|
-
command: {
|
|
86
|
+
command: {
|
|
87
|
+
op: "cache_clean",
|
|
88
|
+
workspace: "",
|
|
89
|
+
image: "",
|
|
90
|
+
at: 3,
|
|
91
|
+
cacheMode: "all",
|
|
92
|
+
},
|
|
79
93
|
});
|
|
80
94
|
await new Promise((resolve) => setImmediate(resolve));
|
|
81
95
|
await scope.update({
|
|
82
|
-
command: {
|
|
96
|
+
command: {
|
|
97
|
+
op: "cache_clean",
|
|
98
|
+
workspace: "",
|
|
99
|
+
image: "",
|
|
100
|
+
at: 4,
|
|
101
|
+
cacheMode: "keep-latest",
|
|
102
|
+
},
|
|
83
103
|
});
|
|
84
104
|
await new Promise((resolve) => setImmediate(resolve));
|
|
85
105
|
const modes = calls
|
|
86
106
|
.filter(([method]) => method === "cleanCaches")
|
|
87
107
|
.map(([, request]) => request.mode);
|
|
88
|
-
assert.deepEqual(modes, [
|
|
108
|
+
assert.deepEqual(modes, [
|
|
109
|
+
"CACHE_CLEAN_MODE_ALL",
|
|
110
|
+
"CACHE_CLEAN_MODE_KEEP_LATEST",
|
|
111
|
+
]);
|
|
89
112
|
assert.equal(scope.value.command, null);
|
|
90
113
|
assert.equal(scope.value.notice, "removed 2 cached files");
|
|
91
114
|
const caches = scope.value.caches;
|
|
@@ -173,7 +196,14 @@ test("secret_remove command drives removeSecret with the name", async () => {
|
|
|
173
196
|
};
|
|
174
197
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
175
198
|
await scope.update({
|
|
176
|
-
command: {
|
|
199
|
+
command: {
|
|
200
|
+
op: "secret_remove",
|
|
201
|
+
workspace: "db-pass",
|
|
202
|
+
image: "",
|
|
203
|
+
at: 7,
|
|
204
|
+
mounts: [],
|
|
205
|
+
value: "",
|
|
206
|
+
},
|
|
177
207
|
});
|
|
178
208
|
await new Promise((resolve) => setImmediate(resolve));
|
|
179
209
|
const removeCall = calls.find(([method]) => method === "removeSecret");
|
|
@@ -203,7 +233,14 @@ test("secret_set command drives writeSecretValue with the name and value", async
|
|
|
203
233
|
};
|
|
204
234
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
205
235
|
await scope.update({
|
|
206
|
-
command: {
|
|
236
|
+
command: {
|
|
237
|
+
op: "secret_set",
|
|
238
|
+
workspace: "db-pass",
|
|
239
|
+
image: "",
|
|
240
|
+
at: 8,
|
|
241
|
+
mounts: [],
|
|
242
|
+
value: "s3cr3t",
|
|
243
|
+
},
|
|
207
244
|
});
|
|
208
245
|
await new Promise((resolve) => setImmediate(resolve));
|
|
209
246
|
const setCall = calls.find(([method]) => method === "writeSecretValue");
|
|
@@ -293,7 +330,12 @@ test("image_base_rebuild command drives rebuildBaseImage with the name", async (
|
|
|
293
330
|
};
|
|
294
331
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
295
332
|
await scope.update({
|
|
296
|
-
command: {
|
|
333
|
+
command: {
|
|
334
|
+
op: "image_base_rebuild",
|
|
335
|
+
workspace: "archlinux",
|
|
336
|
+
image: "",
|
|
337
|
+
at: 21,
|
|
338
|
+
},
|
|
297
339
|
});
|
|
298
340
|
await new Promise((resolve) => setImmediate(resolve));
|
|
299
341
|
const rebuildCall = calls.find(([method]) => method === "rebuildBaseImage");
|
|
@@ -323,7 +365,12 @@ test("image_base_pull command drives pullBaseImage with the name", async () => {
|
|
|
323
365
|
};
|
|
324
366
|
await installCardCommandDriver(fakeContext(scope), resolver);
|
|
325
367
|
await scope.update({
|
|
326
|
-
command: {
|
|
368
|
+
command: {
|
|
369
|
+
op: "image_base_pull",
|
|
370
|
+
workspace: "archlinux",
|
|
371
|
+
image: "",
|
|
372
|
+
at: 22,
|
|
373
|
+
},
|
|
327
374
|
});
|
|
328
375
|
await new Promise((resolve) => setImmediate(resolve));
|
|
329
376
|
const pullCall = calls.find(([method]) => method === "pullBaseImage");
|
|
@@ -398,7 +445,7 @@ test("container_secret_add command drives addContainerSecret with env and secret
|
|
|
398
445
|
workspace: "w",
|
|
399
446
|
container: "web",
|
|
400
447
|
secret: "tok",
|
|
401
|
-
|
|
448
|
+
secretEnvName: "TOKEN",
|
|
402
449
|
image: "",
|
|
403
450
|
at: 16,
|
|
404
451
|
},
|
|
@@ -441,7 +488,7 @@ test("container_secret_add command uses the default container when omitted", asy
|
|
|
441
488
|
workspace: "w",
|
|
442
489
|
container: "",
|
|
443
490
|
secret: "tok",
|
|
444
|
-
|
|
491
|
+
secretEnvName: "TOKEN",
|
|
445
492
|
image: "",
|
|
446
493
|
at: 17,
|
|
447
494
|
},
|
|
@@ -483,7 +530,7 @@ test("container_secret_remove command drives removeContainerSecret with the defa
|
|
|
483
530
|
op: "container_secret_remove",
|
|
484
531
|
workspace: "w",
|
|
485
532
|
container: "",
|
|
486
|
-
|
|
533
|
+
secretEnvName: "TOKEN",
|
|
487
534
|
image: "",
|
|
488
535
|
at: 18,
|
|
489
536
|
},
|
|
@@ -520,7 +567,7 @@ test("unset kind and mode strings map to project and read_only", async () => {
|
|
|
520
567
|
}],
|
|
521
568
|
env: {},
|
|
522
569
|
container: "",
|
|
523
|
-
|
|
570
|
+
secretEnv: {},
|
|
524
571
|
mount: null,
|
|
525
572
|
},
|
|
526
573
|
});
|