@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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/cordis.patch.yml +17 -0
- package/dist/approval-reasons.d.ts +129 -0
- package/dist/approval-reasons.js +253 -0
- package/dist/approval-reasons.test.d.ts +1 -0
- package/dist/approval-reasons.test.js +115 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.js +412 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/approval.test.js +462 -0
- package/dist/card-route.d.ts +15 -0
- package/dist/card-route.js +478 -0
- package/dist/card-route.test.d.ts +1 -0
- package/dist/card-route.test.js +181 -0
- package/dist/card-test-support.d.ts +15 -0
- package/dist/card-test-support.js +160 -0
- package/dist/client/ContainerCard.d.ts +6 -0
- package/dist/client/ContainerCard.js +79 -0
- package/dist/client/card-client.d.ts +6 -0
- package/dist/client/card-client.js +47 -0
- package/dist/client/card-protocol.d.ts +101 -0
- package/dist/client/card-protocol.js +10 -0
- package/dist/client/container-card-caches.d.ts +10 -0
- package/dist/client/container-card-caches.js +31 -0
- package/dist/client/container-card-controller.d.ts +107 -0
- package/dist/client/container-card-controller.js +207 -0
- package/dist/client/container-card-create-modal.d.ts +24 -0
- package/dist/client/container-card-create-modal.js +109 -0
- package/dist/client/container-card-directory-styles.d.ts +5 -0
- package/dist/client/container-card-directory-styles.js +246 -0
- package/dist/client/container-card-directory.d.ts +12 -0
- package/dist/client/container-card-directory.js +144 -0
- package/dist/client/container-card-editors.d.ts +30 -0
- package/dist/client/container-card-editors.js +116 -0
- package/dist/client/container-card-images.d.ts +37 -0
- package/dist/client/container-card-images.js +35 -0
- package/dist/client/container-card-paths.d.ts +20 -0
- package/dist/client/container-card-paths.js +88 -0
- package/dist/client/container-card-row.d.ts +30 -0
- package/dist/client/container-card-row.js +54 -0
- package/dist/client/container-card-secrets.d.ts +21 -0
- package/dist/client/container-card-secrets.js +71 -0
- package/dist/client/container-card-shared.d.ts +55 -0
- package/dist/client/container-card-shared.js +152 -0
- package/dist/client/container-card-styles.d.ts +21 -0
- package/dist/client/container-card-styles.js +143 -0
- package/dist/client/container-card-volumes.d.ts +12 -0
- package/dist/client/container-card-volumes.js +37 -0
- package/dist/client/container-card-workspace.d.ts +31 -0
- package/dist/client/container-card-workspace.js +24 -0
- package/dist/client/directory-picker.d.ts +12 -0
- package/dist/client/directory-picker.js +30 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4419 -0
- package/dist/client/locales.d.ts +9 -0
- package/dist/client/locales.js +370 -0
- package/dist/client/read-only-approval.d.ts +8 -0
- package/dist/client/read-only-approval.js +63 -0
- package/dist/client/slot-contract.d.ts +12 -0
- package/dist/client/slot-contract.js +4 -0
- package/dist/client/terminal-styles.d.ts +5 -0
- package/dist/client/terminal-styles.js +31 -0
- package/dist/client/tool-views.d.ts +8 -0
- package/dist/client/tool-views.js +241 -0
- package/dist/containers.test.d.ts +1 -0
- package/dist/containers.test.js +443 -0
- package/dist/daemons.test.d.ts +1 -0
- package/dist/daemons.test.js +153 -0
- package/dist/fs-provider.d.ts +23 -0
- package/dist/fs-provider.js +184 -0
- package/dist/fs-provider.test.d.ts +1 -0
- package/dist/fs-provider.test.js +260 -0
- package/dist/generated/version.d.ts +2 -0
- package/dist/generated/version.js +6 -0
- package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
- package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
- package/dist/grpc/runtime-client.d.ts +5 -0
- package/dist/grpc/runtime-client.js +43 -0
- package/dist/guest-rpc.d.ts +62 -0
- package/dist/guest-rpc.js +391 -0
- package/dist/images.test.d.ts +1 -0
- package/dist/images.test.js +79 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +140 -0
- package/dist/misc.test.d.ts +1 -0
- package/dist/misc.test.js +141 -0
- package/dist/mount-enums.d.ts +22 -0
- package/dist/mount-enums.js +81 -0
- package/dist/mount-input.d.ts +14 -0
- package/dist/mount-input.js +64 -0
- package/dist/mounts.test.d.ts +1 -0
- package/dist/mounts.test.js +408 -0
- package/dist/output-reader.d.ts +34 -0
- package/dist/output-reader.js +87 -0
- package/dist/paths.test.d.ts +1 -0
- package/dist/paths.test.js +74 -0
- package/dist/preferences.d.ts +6 -0
- package/dist/preferences.js +27 -0
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.js +73 -0
- package/dist/project-path.test.d.ts +1 -0
- package/dist/project-path.test.js +52 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +149 -0
- package/dist/public.d.ts +4 -0
- package/dist/public.js +63 -0
- package/dist/read-only-shell.d.ts +14 -0
- package/dist/read-only-shell.js +150 -0
- package/dist/read-only-shell.test.d.ts +1 -0
- package/dist/read-only-shell.test.js +127 -0
- package/dist/secrets.test.d.ts +1 -0
- package/dist/secrets.test.js +181 -0
- package/dist/settings-commands.test.d.ts +1 -0
- package/dist/settings-commands.test.js +549 -0
- package/dist/settings-create.test.d.ts +1 -0
- package/dist/settings-create.test.js +394 -0
- package/dist/settings-mounts.test.d.ts +1 -0
- package/dist/settings-mounts.test.js +467 -0
- package/dist/settings-paths.test.d.ts +1 -0
- package/dist/settings-paths.test.js +65 -0
- package/dist/settings-schema.d.ts +14 -0
- package/dist/settings-schema.js +14 -0
- package/dist/settings-workspaces.test.d.ts +1 -0
- package/dist/settings-workspaces.test.js +265 -0
- package/dist/spill-store.d.ts +17 -0
- package/dist/spill-store.js +54 -0
- package/dist/spill-store.test.d.ts +1 -0
- package/dist/spill-store.test.js +59 -0
- package/dist/subprocess.d.ts +9 -0
- package/dist/subprocess.js +369 -0
- package/dist/subprocess.test.d.ts +1 -0
- package/dist/subprocess.test.js +234 -0
- package/dist/test-support.d.ts +328 -0
- package/dist/test-support.js +437 -0
- package/dist/tool-defs.d.ts +26 -0
- package/dist/tool-defs.js +188 -0
- package/dist/tool-handlers.d.ts +2 -0
- package/dist/tool-handlers.js +546 -0
- package/dist/tool-params.d.ts +803 -0
- package/dist/tool-params.js +494 -0
- package/dist/tool-schemas.d.ts +2 -0
- package/dist/tool-schemas.js +5 -0
- package/dist/tool-views.d.ts +2 -0
- package/dist/tool-views.js +56 -0
- package/dist/views.test.d.ts +1 -0
- package/dist/views.test.js +23 -0
- package/dist/volumes.test.d.ts +1 -0
- package/dist/volumes.test.js +40 -0
- package/dist/workspace-binding.d.ts +40 -0
- package/dist/workspace-binding.js +275 -0
- package/dist/workspace-binding.test.d.ts +1 -0
- package/dist/workspace-binding.test.js +337 -0
- package/docs/architecture.md +173 -0
- package/docs/architecture.zh.md +129 -0
- package/docs/configuration.md +160 -0
- package/docs/configuration.zh.md +147 -0
- package/docs/development.md +142 -0
- package/docs/development.zh.md +135 -0
- package/docs/install-dsh-and-dsh-podman.md +165 -0
- package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
- package/docs/usage.md +393 -0
- package/docs/usage.zh.md +315 -0
- package/package.json +119 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// projectNameFromHostPath maps an absolute host directory to the project path a
|
|
5
|
+
// project mount stores — the path relative to the projects root. Returns
|
|
6
|
+
// undefined when the directory is not strictly inside the root (the root itself
|
|
7
|
+
// is not a project, and neither is a sibling), so a directory browser can only
|
|
8
|
+
// ever produce a valid mount.
|
|
9
|
+
export function projectNameFromHostPath(projectsRoot, path) {
|
|
10
|
+
const root = stripTrailingSlash(projectsRoot);
|
|
11
|
+
const target = stripTrailingSlash(path);
|
|
12
|
+
if (root === "" || target === "" || target === root)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!target.startsWith(`${root}/`))
|
|
15
|
+
return undefined;
|
|
16
|
+
return target.slice(root.length + 1);
|
|
17
|
+
}
|
|
18
|
+
// hostPathForProjectName is the inverse: the absolute directory a project path
|
|
19
|
+
// names under the projects root, used to open a directory browser where the
|
|
20
|
+
// field's current value lives.
|
|
21
|
+
export function hostPathForProjectName(projectsRoot, project) {
|
|
22
|
+
const root = stripTrailingSlash(projectsRoot);
|
|
23
|
+
const name = stripTrailingSlash(project).replace(/^\/+/, "");
|
|
24
|
+
return name === "" ? root : `${root}/${name}`;
|
|
25
|
+
}
|
|
26
|
+
function stripTrailingSlash(value) {
|
|
27
|
+
return String(value ?? "").replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
// confineToRoot clamps a directory to the projects root subtree, so a stale or
|
|
30
|
+
// hand-edited project path can never open a browser outside it.
|
|
31
|
+
export function confineToRoot(root, path) {
|
|
32
|
+
const normalizedRoot = stripTrailingSlash(root);
|
|
33
|
+
const normalizedPath = stripTrailingSlash(path);
|
|
34
|
+
if (normalizedRoot === "" || normalizedPath === normalizedRoot) {
|
|
35
|
+
return normalizedRoot;
|
|
36
|
+
}
|
|
37
|
+
return normalizedPath.startsWith(`${normalizedRoot}/`)
|
|
38
|
+
? normalizedPath
|
|
39
|
+
: normalizedRoot;
|
|
40
|
+
}
|
|
41
|
+
// parentDirectory is the directory one level above `path`, never above the
|
|
42
|
+
// projects root.
|
|
43
|
+
export function parentDirectory(root, path) {
|
|
44
|
+
const normalizedRoot = stripTrailingSlash(root);
|
|
45
|
+
const index = path.lastIndexOf("/");
|
|
46
|
+
if (index <= 0)
|
|
47
|
+
return normalizedRoot;
|
|
48
|
+
const parent = stripTrailingSlash(path.slice(0, index));
|
|
49
|
+
return parent.length < normalizedRoot.length ? normalizedRoot : parent;
|
|
50
|
+
}
|
|
51
|
+
// rootCrumbs returns the cumulative paths from the projects root down to `path`
|
|
52
|
+
// inclusive, so a breadcrumb trail never renders a segment above the root.
|
|
53
|
+
export function rootCrumbs(root, path) {
|
|
54
|
+
const normalizedRoot = stripTrailingSlash(root);
|
|
55
|
+
const prefix = `${normalizedRoot}/`;
|
|
56
|
+
if (normalizedRoot === "" || !path.startsWith(prefix))
|
|
57
|
+
return [];
|
|
58
|
+
const crumbs = [];
|
|
59
|
+
let accumulated = normalizedRoot;
|
|
60
|
+
for (const segment of path.slice(prefix.length).split("/")) {
|
|
61
|
+
if (segment === "")
|
|
62
|
+
continue;
|
|
63
|
+
accumulated = `${accumulated}/${segment}`;
|
|
64
|
+
crumbs.push(accumulated);
|
|
65
|
+
}
|
|
66
|
+
return crumbs;
|
|
67
|
+
}
|
|
68
|
+
// crumbLabel is the display name of one crumb path.
|
|
69
|
+
export function crumbLabel(path) {
|
|
70
|
+
const trimmed = stripTrailingSlash(path);
|
|
71
|
+
const name = trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
72
|
+
return name === "" ? trimmed : name;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { confineToRoot, crumbLabel, hostPathForProjectName, parentDirectory, projectNameFromHostPath, rootCrumbs, } from "./project-path.js";
|
|
7
|
+
test("projectNameFromHostPath maps a host directory to a project path", () => {
|
|
8
|
+
assert.equal(projectNameFromHostPath("/projects", "/projects/web"), "web");
|
|
9
|
+
assert.equal(projectNameFromHostPath("/projects", "/projects/group/web"), "group/web");
|
|
10
|
+
assert.equal(projectNameFromHostPath("/projects/", "/projects/web/"), "web", "trailing slashes are ignored");
|
|
11
|
+
});
|
|
12
|
+
test("projectNameFromHostPath refuses anything outside the projects root", () => {
|
|
13
|
+
assert.equal(projectNameFromHostPath("/projects", "/projects"), undefined);
|
|
14
|
+
assert.equal(projectNameFromHostPath("/projects", "/projects/"), undefined);
|
|
15
|
+
assert.equal(projectNameFromHostPath("/projects", "/projects2/web"), undefined);
|
|
16
|
+
assert.equal(projectNameFromHostPath("/projects", "/etc/hosts"), undefined);
|
|
17
|
+
assert.equal(projectNameFromHostPath("/projects", "web"), undefined);
|
|
18
|
+
assert.equal(projectNameFromHostPath("/projects", ""), undefined);
|
|
19
|
+
assert.equal(projectNameFromHostPath("", "/projects/web"), undefined);
|
|
20
|
+
});
|
|
21
|
+
test("hostPathForProjectName is the inverse", () => {
|
|
22
|
+
assert.equal(hostPathForProjectName("/projects", "web"), "/projects/web");
|
|
23
|
+
assert.equal(hostPathForProjectName("/projects", "group/web"), "/projects/group/web");
|
|
24
|
+
assert.equal(hostPathForProjectName("/projects", ""), "/projects");
|
|
25
|
+
assert.equal(hostPathForProjectName("/projects", "/web"), "/projects/web");
|
|
26
|
+
});
|
|
27
|
+
test("confineToRoot clamps a directory to the projects root", () => {
|
|
28
|
+
assert.equal(confineToRoot("/projects", "/projects/web"), "/projects/web");
|
|
29
|
+
assert.equal(confineToRoot("/projects", "/projects"), "/projects");
|
|
30
|
+
assert.equal(confineToRoot("/projects", "/etc"), "/projects");
|
|
31
|
+
assert.equal(confineToRoot("/projects", ""), "/projects");
|
|
32
|
+
assert.equal(confineToRoot("/projects", "/projects/web/"), "/projects/web", "trailing slashes are ignored");
|
|
33
|
+
});
|
|
34
|
+
test("parentDirectory never climbs above the projects root", () => {
|
|
35
|
+
assert.equal(parentDirectory("/projects", "/projects/web/sub"), "/projects/web");
|
|
36
|
+
assert.equal(parentDirectory("/projects", "/projects/web"), "/projects");
|
|
37
|
+
assert.equal(parentDirectory("/projects", "/projects"), "/projects");
|
|
38
|
+
assert.equal(parentDirectory("/projects", "/elsewhere"), "/projects");
|
|
39
|
+
});
|
|
40
|
+
test("rootCrumbs lists the root-relative ancestry", () => {
|
|
41
|
+
assert.deepEqual(rootCrumbs("/projects", "/projects/group/web"), [
|
|
42
|
+
"/projects/group",
|
|
43
|
+
"/projects/group/web",
|
|
44
|
+
]);
|
|
45
|
+
assert.deepEqual(rootCrumbs("/projects", "/projects"), []);
|
|
46
|
+
assert.deepEqual(rootCrumbs("/projects", "/etc"), [], "a path outside the root has no crumbs");
|
|
47
|
+
});
|
|
48
|
+
test("crumbLabel is the last segment", () => {
|
|
49
|
+
assert.equal(crumbLabel("/projects/group/web"), "web");
|
|
50
|
+
assert.equal(crumbLabel("/projects/group/web/"), "web");
|
|
51
|
+
assert.equal(crumbLabel("/projects"), "projects");
|
|
52
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const HARNESS_SOURCE_SECTION = "harness:source";
|
|
2
|
+
export declare function withoutHarnessSourceSection(assembly: any): any;
|
|
3
|
+
export declare function podmanRuntimeSection(ctx: any): {
|
|
4
|
+
name: string;
|
|
5
|
+
order: number;
|
|
6
|
+
text: (context?: {
|
|
7
|
+
scope?: unknown;
|
|
8
|
+
}) => string;
|
|
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
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
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
|
+
// Prompt section the harness registers to name its own on-disk checkout.
|
|
9
|
+
// Mirrors @deepseek-ai/dsh-app-boot's HARNESS_SOURCE_SECTION.
|
|
10
|
+
export const HARNESS_SOURCE_SECTION = "harness:source";
|
|
11
|
+
// The checkout the section points at lives on the host and is never reachable
|
|
12
|
+
// from the workspace container, so the line is false under this plugin. Drop it
|
|
13
|
+
// from the assembled prompt without touching any other section.
|
|
14
|
+
export function withoutHarnessSourceSection(assembly) {
|
|
15
|
+
return {
|
|
16
|
+
...assembly,
|
|
17
|
+
sections: assembly.sections.filter((section) => section.name !== HARNESS_SOURCE_SECTION),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// The built-in shell and filesystem tools whose presence decides which runtime
|
|
21
|
+
// wording applies.
|
|
22
|
+
const BUILTIN_TOOLS = ["bash", "read", "write", "edit", "glob", "grep"];
|
|
23
|
+
// Wording for an agent that has the built-in shell and filesystem tools.
|
|
24
|
+
const RUNTIME_WITH_BUILTINS = "This dsh session's `bash`, `read`, `write`, `edit`, `glob`, and `grep` " +
|
|
25
|
+
"are container-backed: they run in a Podman container, the default " +
|
|
26
|
+
"container of the current dsh workspace — the same one `container_bash` " +
|
|
27
|
+
"and the other container-scoped tools (`container_*` and `daemon_*`) " +
|
|
28
|
+
'target with `container: "default"`. Containers are scoped to a dsh ' +
|
|
29
|
+
"workspace: each workspace has its own default container and any named " +
|
|
30
|
+
"ones, and the container-scoped tools address them by logical name within " +
|
|
31
|
+
"the current workspace. There is no host shell — never describe their " +
|
|
32
|
+
"output as the host's — and host paths do not exist. Because `bash` and " +
|
|
33
|
+
"`container_bash` run in that one container with the same command-visible " +
|
|
34
|
+
"environment, a command sees the same environment and filesystem either " +
|
|
35
|
+
"way, and matching output is never evidence of a host " +
|
|
36
|
+
"shell. Containers share the host kernel, so `uname -a`, `uname -r`, and " +
|
|
37
|
+
"`/proc/version` do report the host kernel; the " +
|
|
38
|
+
"container's own identity shows in `hostname`, `/etc/os-release`, and " +
|
|
39
|
+
"`/proc/1/cmdline`.";
|
|
40
|
+
// Wording for an agent that has only the container tools (the Podman operator
|
|
41
|
+
// preset mounts none of the built-in shell or filesystem rows), so it never
|
|
42
|
+
// names a tool the agent does not have.
|
|
43
|
+
const RUNTIME_CONTAINER_ONLY = "This dsh session's container tools run in a Podman container: the default " +
|
|
44
|
+
"container of the current dsh workspace — the same one the container-scoped " +
|
|
45
|
+
'tools (`container_*` and `daemon_*`) target with `container: "default"`. ' +
|
|
46
|
+
"Containers are scoped to a dsh workspace: each workspace has its own " +
|
|
47
|
+
"default container and any named ones, and the container-scoped tools " +
|
|
48
|
+
"address them by logical name within the current workspace. There is no " +
|
|
49
|
+
"host shell — never describe their output as the host's — and host paths do " +
|
|
50
|
+
"not exist. Containers share the host kernel, so `uname -a`, `uname -r`, " +
|
|
51
|
+
"and `/proc/version` do report the host kernel; the container's own identity " +
|
|
52
|
+
"shows in `hostname`, `/etc/os-release`, and `/proc/1/cmdline`.";
|
|
53
|
+
// Correct the model's host/container mental model: the built-in shell and
|
|
54
|
+
// filesystem tools are container-backed too, so there is no host shell. It sits
|
|
55
|
+
// just before the tool sections so the correction lands next to the tool
|
|
56
|
+
// descriptions it explains, and it states the two facts that otherwise invite a
|
|
57
|
+
// "bash runs on the host" hallucination: `bash` and `container_bash` use the
|
|
58
|
+
// same container, and a container shares the host kernel (so `uname` matches
|
|
59
|
+
// the host even though the container's identity does not). It names the
|
|
60
|
+
// execution environment a Podman container, not a "Podman workspace", and says
|
|
61
|
+
// that containers are scoped to a dsh workspace, so the two vocabularies stay
|
|
62
|
+
// apart. An agent without the built-in tools gets the container-only wording.
|
|
63
|
+
export function podmanRuntimeSection(ctx) {
|
|
64
|
+
return {
|
|
65
|
+
name: "podman:runtime",
|
|
66
|
+
order: 950,
|
|
67
|
+
text: ({ scope } = {}) => BUILTIN_TOOLS.some((name) => ctx.tools.get(name, scope) !== undefined)
|
|
68
|
+
? RUNTIME_WITH_BUILTINS
|
|
69
|
+
: RUNTIME_CONTAINER_ONLY,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
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/public.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function publicMount(mount: any): Record<string, unknown>;
|
|
2
|
+
export declare function publicContainer(row: any): Record<string, unknown>;
|
|
3
|
+
export declare function publicImage(image: any): Record<string, unknown>;
|
|
4
|
+
export declare function publicDaemon(info: any): Record<string, unknown>;
|
package/dist/public.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { mountKindFromProto, mountModeFromProto } from "./mount-enums.js";
|
|
5
|
+
// Rebuild API objects so tool results never expose internal fields (see
|
|
6
|
+
// AGENTS.md "Security"): only allow-listed attributes reach the model.
|
|
7
|
+
export function publicMount(mount) {
|
|
8
|
+
const kind = mountKindFromProto(mount?.kind);
|
|
9
|
+
const mode = mountModeFromProto(mount?.mode);
|
|
10
|
+
return {
|
|
11
|
+
...(mount?.projectName ? { projectName: mount.projectName } : {}),
|
|
12
|
+
...(mount?.destination ? { destination: mount.destination } : {}),
|
|
13
|
+
...(mount?.volume ? { volume: mount.volume } : {}),
|
|
14
|
+
...(mount?.secret ? { secret: mount.secret } : {}),
|
|
15
|
+
...(kind !== undefined ? { kind } : {}),
|
|
16
|
+
...(mode !== undefined ? { mode } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function publicContainer(row) {
|
|
20
|
+
return {
|
|
21
|
+
containerName: row?.containerName ?? "",
|
|
22
|
+
status: row?.status ?? "",
|
|
23
|
+
...(row?.imageId ? { imageId: row.imageId } : {}),
|
|
24
|
+
mounts: (row?.mounts ?? []).map(publicMount),
|
|
25
|
+
paths: row?.paths ?? [],
|
|
26
|
+
env: row?.env ?? {},
|
|
27
|
+
secretEnv: row?.secretEnv ?? {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function publicImage(image) {
|
|
31
|
+
return {
|
|
32
|
+
imageId: image?.imageId ?? "",
|
|
33
|
+
...(image?.parent ? { parent: image.parent } : {}),
|
|
34
|
+
packages: image?.packages ?? [],
|
|
35
|
+
isBase: image?.isBase ?? false,
|
|
36
|
+
status: image?.status ?? "",
|
|
37
|
+
...(image?.primitive ? { primitive: image.primitive } : {}),
|
|
38
|
+
...(image?.packageManager ? { packageManager: image.packageManager } : {}),
|
|
39
|
+
...(image?.builtAt ? { builtAt: image.builtAt } : {}),
|
|
40
|
+
basePublic: image?.basePublic ?? false,
|
|
41
|
+
...(image?.imageTag ? { imageTag: image.imageTag } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function publicDaemon(info) {
|
|
45
|
+
const running = info?.running ?? false;
|
|
46
|
+
return {
|
|
47
|
+
name: info?.name ?? "",
|
|
48
|
+
argv: info?.argv ?? [],
|
|
49
|
+
running,
|
|
50
|
+
// An exit code only means something once the daemon has exited; the proto
|
|
51
|
+
// field defaults to 0 while it runs.
|
|
52
|
+
...(!running && info?.exitCode !== undefined && info?.exitCode !== null
|
|
53
|
+
? { exitCode: info.exitCode }
|
|
54
|
+
: {}),
|
|
55
|
+
...(info?.startedAt ? { startedAt: info.startedAt } : {}),
|
|
56
|
+
...(info?.stoppedAt ? { stoppedAt: info.stoppedAt } : {}),
|
|
57
|
+
...(info?.uid !== undefined && info?.uid !== null ? { uid: info.uid } : {}),
|
|
58
|
+
...(info?.gid !== undefined && info?.gid !== null ? { gid: info.gid } : {}),
|
|
59
|
+
...(Array.isArray(info?.groups) && info.groups.length > 0
|
|
60
|
+
? { groups: info.groups }
|
|
61
|
+
: {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReasonLocale } from "./approval-reasons.js";
|
|
2
|
+
import { type WorkspaceResolver } from "./workspace-binding.js";
|
|
3
|
+
export declare const REMOUNT_TOOL_NAME = "dsh_podman_builtin_remount_read_only";
|
|
4
|
+
export declare const READ_ONLY_GATED_TOOLS: ReadonlySet<string>;
|
|
5
|
+
export type ReadOnlyShellDecision = {
|
|
6
|
+
kind: "allow";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "deny";
|
|
9
|
+
reason: string;
|
|
10
|
+
} | undefined;
|
|
11
|
+
export declare function createReadOnlyShellGate(deps: {
|
|
12
|
+
resolver: WorkspaceResolver;
|
|
13
|
+
approve: (exec: any, reason: string) => Promise<boolean>;
|
|
14
|
+
}): (exec: any, locale: ReasonLocale) => Promise<ReadOnlyShellDecision>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { currentCwd, sessionWorkspaceSlug } from "./guest-rpc.js";
|
|
5
|
+
import { mountKindFromProto, mountKindToProto } from "./mount-enums.js";
|
|
6
|
+
import { renderDenial, renderReason, } from "./approval-reasons.js";
|
|
7
|
+
// The synthetic tool name the read-only remount prompt is raised under. The
|
|
8
|
+
// `dsh_podman_builtin_` prefix is reserved for plugin-raised prompts about
|
|
9
|
+
// built-in operations, so the browser panel can recognise them without
|
|
10
|
+
// mistaking a real tool for one.
|
|
11
|
+
export const REMOUNT_TOOL_NAME = "dsh_podman_builtin_remount_read_only";
|
|
12
|
+
// The tools the read-only gate covers: the built-in shell and file tools, and
|
|
13
|
+
// the plugin's counterparts plus daemons (everything that would otherwise be
|
|
14
|
+
// denied by read-only and can run or write through the container's mounts).
|
|
15
|
+
export const READ_ONLY_GATED_TOOLS = new Set([
|
|
16
|
+
"bash",
|
|
17
|
+
"pwsh",
|
|
18
|
+
"write",
|
|
19
|
+
"edit",
|
|
20
|
+
"container_bash",
|
|
21
|
+
"container_exec",
|
|
22
|
+
"container_write",
|
|
23
|
+
"container_edit",
|
|
24
|
+
"daemon_start",
|
|
25
|
+
]);
|
|
26
|
+
// The read-only shell gate: under read-only permission, the covered tools run
|
|
27
|
+
// once every mount that carries a mode is read-only. Otherwise the user is
|
|
28
|
+
// asked (through `approve`) to remount the read-write mounts read-only; on
|
|
29
|
+
// approval the container is recreated with the read-only list and the tool runs.
|
|
30
|
+
export function createReadOnlyShellGate(deps) {
|
|
31
|
+
return async (exec, locale) => {
|
|
32
|
+
const name = String(exec?.name ?? "");
|
|
33
|
+
if (!READ_ONLY_GATED_TOOLS.has(name))
|
|
34
|
+
return undefined;
|
|
35
|
+
const container = containerOf(exec);
|
|
36
|
+
const state = await readContainerMounts(deps.resolver, exec, container);
|
|
37
|
+
// An unreadable container falls through to the caller's plain denial: the
|
|
38
|
+
// gate never widens access it could not verify.
|
|
39
|
+
if (state === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const remount = state.mounts.filter((mount) => carriesMode(mount.kind) && mount.mode !== "MOUNT_MODE_READ_ONLY");
|
|
42
|
+
if (remount.length === 0)
|
|
43
|
+
return { kind: "allow" };
|
|
44
|
+
const keep = state.mounts.filter((mount) => !remount.includes(mount));
|
|
45
|
+
const reason = renderReason(locale, {
|
|
46
|
+
kind: "read_only_remount",
|
|
47
|
+
tool: name,
|
|
48
|
+
remount: remount.map(mountFactOf),
|
|
49
|
+
keep: keep.map(mountFactOf),
|
|
50
|
+
});
|
|
51
|
+
if (!(await deps.approve(exec, reason))) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "deny",
|
|
54
|
+
reason: renderDenial(locale, {
|
|
55
|
+
kind: "read_only_remount_declined",
|
|
56
|
+
tool: name,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
await remountReadOnly(deps.resolver, exec, container, state.mounts);
|
|
61
|
+
return { kind: "allow" };
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// The logical container a tool call targets: its `container` argument for the
|
|
65
|
+
// plugin's tools, the default container for the built-in ones.
|
|
66
|
+
function containerOf(exec) {
|
|
67
|
+
const container = exec?.arguments?.container;
|
|
68
|
+
return typeof container === "string" && container !== "" ? container : "default";
|
|
69
|
+
}
|
|
70
|
+
// Whether a mount kind carries a mode: project and volume mounts do; tmpfs is
|
|
71
|
+
// always read-write and secret mounts take none.
|
|
72
|
+
function carriesMode(kind) {
|
|
73
|
+
return (kind === undefined ||
|
|
74
|
+
kind === "" ||
|
|
75
|
+
kind === "MOUNT_KIND_PROJECT" ||
|
|
76
|
+
kind === "MOUNT_KIND_VOLUME");
|
|
77
|
+
}
|
|
78
|
+
// readContainerMounts resolves the workspace and returns the target container's
|
|
79
|
+
// mounts, or undefined when the container cannot be read.
|
|
80
|
+
async function readContainerMounts(resolver, exec, container) {
|
|
81
|
+
try {
|
|
82
|
+
const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
|
|
83
|
+
const result = await resolver.control("listContainers", {});
|
|
84
|
+
const row = (result.containers ?? []).find((candidate) => candidate.workspaceSlug === slug &&
|
|
85
|
+
candidate.containerName === container);
|
|
86
|
+
if (row === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
return { slug, mounts: (row.mounts ?? []) };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// remountReadOnly recreates the container with every project and volume mount
|
|
95
|
+
// read-only; tmpfs stays read-write and secrets carry no mode. One recreate
|
|
96
|
+
// replaces the whole list, so the mounts never pass through an inconsistent
|
|
97
|
+
// state.
|
|
98
|
+
async function remountReadOnly(resolver, exec, container, mounts) {
|
|
99
|
+
await resolver.control("recreateContainer", {
|
|
100
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
101
|
+
container,
|
|
102
|
+
mounts: mounts.map(readOnlyMountRequest),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// readOnlyMountRequest maps a stored mount onto the control plane's mount
|
|
106
|
+
// message with the mode forced read-only.
|
|
107
|
+
function readOnlyMountRequest(mount) {
|
|
108
|
+
const kind = mountKindFromProto(typeof mount.kind === "string" ? mount.kind : undefined);
|
|
109
|
+
const request = {
|
|
110
|
+
projectName: typeof mount.projectName === "string" ? mount.projectName : "",
|
|
111
|
+
kind: mountKindToProto(kind),
|
|
112
|
+
};
|
|
113
|
+
if (carriesMode(mount.kind))
|
|
114
|
+
request.mode = "MOUNT_MODE_READ_ONLY";
|
|
115
|
+
else if (kind === "tmpfs")
|
|
116
|
+
request.mode = "MOUNT_MODE_READ_WRITE";
|
|
117
|
+
if (typeof mount.destination === "string" && mount.destination !== "") {
|
|
118
|
+
request.destination = mount.destination;
|
|
119
|
+
}
|
|
120
|
+
if (typeof mount.volume === "string" && mount.volume !== "") {
|
|
121
|
+
request.volume = mount.volume;
|
|
122
|
+
}
|
|
123
|
+
if (typeof mount.secret === "string" && mount.secret !== "") {
|
|
124
|
+
request.secret = mount.secret;
|
|
125
|
+
}
|
|
126
|
+
return request;
|
|
127
|
+
}
|
|
128
|
+
// mountFactOf renders one stored mount as an approval reason fact.
|
|
129
|
+
function mountFactOf(mount) {
|
|
130
|
+
const resolved = mountKindFromProto(typeof mount.kind === "string" ? mount.kind : undefined);
|
|
131
|
+
const kind = resolved === "volume" || resolved === "secret" || resolved === "tmpfs"
|
|
132
|
+
? resolved
|
|
133
|
+
: "project";
|
|
134
|
+
const destination = typeof mount.destination === "string" && mount.destination !== ""
|
|
135
|
+
? mount.destination
|
|
136
|
+
: undefined;
|
|
137
|
+
const source = kind === "volume"
|
|
138
|
+
? String(mount.volume ?? "")
|
|
139
|
+
: kind === "secret"
|
|
140
|
+
? String(mount.secret ?? "")
|
|
141
|
+
: kind === "tmpfs"
|
|
142
|
+
? ""
|
|
143
|
+
: String(mount.projectName ?? "");
|
|
144
|
+
return {
|
|
145
|
+
kind,
|
|
146
|
+
source,
|
|
147
|
+
...(destination === undefined ? {} : { destination }),
|
|
148
|
+
readOnly: mount.mode === "MOUNT_MODE_READ_ONLY",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|