@exagone313/dsh-podman 0.3.0 → 0.4.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.
@@ -2,7 +2,7 @@ import { type CardSnapshot } from "./client/card-protocol.js";
2
2
  import { type ReasonLocale } from "./approval-reasons.js";
3
3
  import { type WorkspaceResolver } from "./workspace-binding.js";
4
4
  /** Build the live snapshot the card renders. */
5
- export declare function cardSnapshot(resolver: WorkspaceResolver, workspaceRegistry: any): Promise<CardSnapshot>;
5
+ export declare function cardSnapshot(resolver: WorkspaceResolver, workspaceRegistry: any, harnessVersion?: string): Promise<CardSnapshot>;
6
6
  /**
7
7
  * Run one card command, returning the notice to show (usually empty). `ctx` is
8
8
  * the settings-injected context, read only for the session locale.
@@ -13,4 +13,4 @@ export declare function runCommand(_ctx: any, resolver: WorkspaceResolver, works
13
13
  * harness API path, so the carrier applies its Host/Origin fence and browser
14
14
  * authentication before the handler runs.
15
15
  */
16
- export declare function registerCardRoute(ctx: any, resolver: WorkspaceResolver, workspaceRegistry?: any, readLocale?: () => ReasonLocale): void;
16
+ export declare function registerCardRoute(ctx: any, resolver: WorkspaceResolver, workspaceRegistry?: any, readLocale?: () => ReasonLocale, harnessVersion?: string): void;
@@ -131,7 +131,7 @@ function dshWorkspaceCwd(registry, slug) {
131
131
  throw new Error(`unknown workspace ${JSON.stringify(slug)}`);
132
132
  }
133
133
  /** Build the live snapshot the card renders. */
134
- export async function cardSnapshot(resolver, workspaceRegistry) {
134
+ export async function cardSnapshot(resolver, workspaceRegistry, harnessVersion = dshVersion()) {
135
135
  // The handshake RPC is exempt from the orchestrator's version check, so it
136
136
  // succeeds even when the rest is refused; an older orchestrator that has no
137
137
  // GetVersion yet leaves the version unknown.
@@ -156,7 +156,7 @@ export async function cardSnapshot(resolver, workspaceRegistry) {
156
156
  return {
157
157
  version: VERSION,
158
158
  commit: GIT_COMMIT,
159
- dshVersion: dshVersion(),
159
+ dshVersion: harnessVersion,
160
160
  orchestratorVersion,
161
161
  versionState: "major-mismatch",
162
162
  projectsRoot: resolver.getConfig().projectsRoot,
@@ -172,7 +172,7 @@ export async function cardSnapshot(resolver, workspaceRegistry) {
172
172
  return {
173
173
  version: VERSION,
174
174
  commit: GIT_COMMIT,
175
- dshVersion: dshVersion(),
175
+ dshVersion: harnessVersion,
176
176
  orchestratorVersion,
177
177
  versionState: pluginVersionState(VERSION, orchestratorVersion),
178
178
  projectsRoot: resolver.getConfig().projectsRoot,
@@ -537,10 +537,10 @@ function normalizeCommand(raw) {
537
537
  mount: mountField(raw.mount),
538
538
  };
539
539
  }
540
- async function handleCardRequest(request, ctx, resolver, workspaceRegistry, readLocale) {
540
+ async function handleCardRequest(request, ctx, resolver, workspaceRegistry, readLocale, harnessVersion) {
541
541
  if (request.method === "GET") {
542
542
  try {
543
- return jsonResponse(200, await cardSnapshot(resolver, workspaceRegistry));
543
+ return jsonResponse(200, await cardSnapshot(resolver, workspaceRegistry, harnessVersion));
544
544
  }
545
545
  catch (error) {
546
546
  return jsonResponse(500, {
@@ -578,13 +578,13 @@ async function handleCardRequest(request, ctx, resolver, workspaceRegistry, read
578
578
  * harness API path, so the carrier applies its Host/Origin fence and browser
579
579
  * authentication before the handler runs.
580
580
  */
581
- export function registerCardRoute(ctx, resolver, workspaceRegistry, readLocale) {
581
+ export function registerCardRoute(ctx, resolver, workspaceRegistry, readLocale, harnessVersion) {
582
582
  ctx.inject(["connection"], (connectionCtx) => {
583
583
  connectionCtx.connection.fetch.register({
584
584
  path: CARD_PATH,
585
585
  methods: ["GET", "POST"],
586
586
  requestBody: "buffered",
587
- fetch: (request) => handleCardRequest(request, connectionCtx, resolver, workspaceRegistry, readLocale),
587
+ fetch: (request) => handleCardRequest(request, connectionCtx, resolver, workspaceRegistry, readLocale, harnessVersion),
588
588
  });
589
589
  });
590
590
  }
@@ -1 +1,7 @@
1
- export declare function dshVersion(): string;
1
+ export interface PluginPackageLookup {
2
+ packageOf?(specifier: string, parentURL: string): {
3
+ version?: unknown;
4
+ } | undefined;
5
+ }
6
+ export declare function dshVersion(packages?: PluginPackageLookup): string;
7
+ export declare function versionFromPackages(packages?: PluginPackageLookup): string | undefined;
@@ -4,24 +4,37 @@
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- // The harness version this plugin runs inside. Every `@deepseek-ai/dsh-*`
8
- // package carries the release version, and the dsh app package is linked into
9
- // the profile's healed node_modules, so it is resolvable from the plugin even
10
- // though it is not a dependency of it. "" when it cannot be resolved, which the
11
- // card renders as unknown.
7
+ // The harness version this plugin runs inside. The dsh app package is not a
8
+ // physical dependency of the plugin: the boot loader serves profile plugin
9
+ // imports from an in-memory package table, so the plugin asks that table
10
+ // (`ctx.pluginPackages`) for the package it runs under. The physical module
11
+ // walk below is only a fallback for a development checkout. "" when the
12
+ // version cannot be resolved, which the card renders as unknown.
13
+ const PACKAGE = "@deepseek-ai/dsh";
12
14
  let cached;
13
- export function dshVersion() {
14
- cached ??= resolveDshVersion() ?? "";
15
- return cached;
15
+ export function dshVersion(packages) {
16
+ if (cached !== undefined)
17
+ return cached;
18
+ const resolved = versionFromPackages(packages) ?? resolveDshVersion();
19
+ // Only a resolved version is cached, so an early lookup without the package
20
+ // table cannot pin "" for the process lifetime.
21
+ if (resolved !== undefined && resolved !== "")
22
+ cached = resolved;
23
+ return resolved ?? "";
16
24
  }
17
- // resolveDshVersion walks the module search paths the way the harness resolves
18
- // its own bundles, so a package that does not export ./package.json still
19
- // resolves.
25
+ // versionFromPackages reads the running app package's manifest version through
26
+ // the profile package lookup. It is undefined when the lookup is absent, the
27
+ // package is unknown, or the manifest carries no usable version.
28
+ export function versionFromPackages(packages) {
29
+ const version = packages?.packageOf?.(PACKAGE, import.meta.url)?.version;
30
+ return typeof version === "string" && version !== "" ? version : undefined;
31
+ }
32
+ // resolveDshVersion walks the physical module search paths, for an install
33
+ // whose app package is a real node_modules entry (a source checkout).
20
34
  function resolveDshVersion() {
21
35
  let searchPaths;
22
36
  try {
23
- searchPaths =
24
- createRequire(import.meta.url).resolve.paths("@deepseek-ai/dsh") ?? [];
37
+ searchPaths = createRequire(import.meta.url).resolve.paths(PACKAGE) ?? [];
25
38
  }
26
39
  catch {
27
40
  return undefined;
@@ -29,7 +42,7 @@ function resolveDshVersion() {
29
42
  for (const searchPath of searchPaths) {
30
43
  if (searchPath === null)
31
44
  continue;
32
- const manifest = join(searchPath, "@deepseek-ai/dsh", "package.json");
45
+ const manifest = join(searchPath, PACKAGE, "package.json");
33
46
  if (!existsSync(manifest))
34
47
  continue;
35
48
  try {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
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 { versionFromPackages } from "./dsh-version.js";
7
+ test("versionFromPackages reads the app version through the profile lookup", () => {
8
+ const packages = {
9
+ packageOf: (specifier, parentURL) => {
10
+ assert.equal(specifier, "@deepseek-ai/dsh");
11
+ assert.ok(parentURL.startsWith("file:"), "lookup uses this module's URL");
12
+ return { version: "1.2.3" };
13
+ },
14
+ };
15
+ assert.equal(versionFromPackages(packages), "1.2.3");
16
+ });
17
+ test("versionFromPackages reports no version when the package is unknown", () => {
18
+ assert.equal(versionFromPackages({ packageOf: () => undefined }), undefined);
19
+ });
20
+ test("versionFromPackages ignores a missing lookup and unusable versions", () => {
21
+ assert.equal(versionFromPackages(undefined), undefined);
22
+ assert.equal(versionFromPackages({}), undefined);
23
+ assert.equal(versionFromPackages({ packageOf: () => ({ version: "" }) }), undefined);
24
+ assert.equal(versionFromPackages({ packageOf: () => ({ version: 3 }) }), undefined);
25
+ });
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.3.0";
2
- export declare const GIT_COMMIT = "7db83bb";
1
+ export declare const VERSION = "0.4.0";
2
+ export declare const GIT_COMMIT = "e5c1eb5";
@@ -2,5 +2,5 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
  // Generated by scripts/generate-version.mjs; do not edit.
5
- export const VERSION = "0.3.0";
6
- export const GIT_COMMIT = "7db83bb";
5
+ export const VERSION = "0.4.0";
6
+ export const GIT_COMMIT = "e5c1eb5";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { toolHandlers } from "./tool-handlers.js";
12
12
  import { defineTool, TOOL_DESCRIPTIONS, toolOutput, TOOLS, } from "./tool-schemas.js";
13
13
  import { toolCallView, toolResultView } from "./tool-views.js";
14
14
  import { registerCardRoute } from "./card-route.js";
15
+ import { dshVersion } from "./dsh-version.js";
15
16
  import { registerTerminalRoutes } from "./terminal-route.js";
16
17
  import { TerminalSessions } from "./terminal-sessions.js";
17
18
  import { normalizeToolError, WorkspaceResolver } from "./workspace-binding.js";
@@ -103,7 +104,11 @@ export function apply(ctx, config) {
103
104
  ctx.inject(["settings"], (child) => {
104
105
  child.effect(() => child.settings.configure({ auto: false }, ctx.fiber), "podman: settings presentation");
105
106
  });
106
- registerCardRoute(ctx, resolver, ctx.workspaceRegistry, readLocale);
107
+ // The harness version is read through the profile's package lookup: the dsh
108
+ // app package is served from the loader's in-memory table, not a physical
109
+ // node_modules entry. It is resolved once and served with every snapshot.
110
+ const dshVersionValue = dshVersion(ctx.get("pluginPackages"));
111
+ registerCardRoute(ctx, resolver, ctx.workspaceRegistry, readLocale, dshVersionValue);
107
112
  // The Podman terminal tab talks to guest ptys through its own routes; the
108
113
  // registry keeps shells alive while the browser is away.
109
114
  const terminalSessions = new TerminalSessions({ resolver });
@@ -8,7 +8,7 @@ import loader from "@grpc/proto-loader";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { dirname, resolve } from "node:path";
10
10
  import { tmpdir } from "node:os";
11
- import { mkdirSync } from "node:fs";
11
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
12
12
  import { controlClient, guestClient } from "./grpc/runtime-client.js";
13
13
  import { containerNotFound, metadata, normalizeToolError, WorkspaceResolver, workspaceSlug, } from "./workspace-binding.js";
14
14
  import { VERSION } from "./generated/version.js";
@@ -190,9 +190,20 @@ async function startControlServer(containers = [], options = {}) {
190
190
  versions,
191
191
  createRequests,
192
192
  recreateRequests,
193
- stop: () => server.forceShutdown(),
193
+ stop: () => {
194
+ server.forceShutdown();
195
+ // gRPC unlinks its sockets on shutdown; drop the root too so repeated
196
+ // runs do not accumulate directories under the system temp dir.
197
+ rmSync(socketsRoot, { recursive: true, force: true });
198
+ },
194
199
  };
195
200
  }
201
+ test("the control server cleanup removes its temp root", async () => {
202
+ const control = await startControlServer();
203
+ assert.ok(existsSync(control.socketsRoot));
204
+ control.stop();
205
+ assert.equal(existsSync(control.socketsRoot), false);
206
+ });
196
207
  test("control calls carry the bearer token", async () => {
197
208
  const { socketsRoot, received, versions, stop } = await startControlServer();
198
209
  try {
package/docs/usage.md CHANGED
@@ -49,6 +49,18 @@ dsh-podman organizes the images its containers run into three tiers:
49
49
  Image references (`imageId`, `parent`, `image`) are **short names only** (no
50
50
  registry prefix, no `:tag`).
51
51
 
52
+ ### Base images
53
+
54
+ Each base image is built from its primitive reference with the same fixed,
55
+ dsh-podman-owned package list, and a custom image inherits its parent's package
56
+ manager.
57
+
58
+ | Short name | Package manager | Primitive | Packages |
59
+ | ----------- | --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `archlinux` | `pacman` | `docker.io/library/archlinux:latest` | `base-devel`, `ca-certificates`, `curl`, `diffutils`, `fd`, `git`, `inetutils`, `jq`, `less`, `openbsd-netcat`, `openssh`, `patch`, `procps-ng`, `python`, `ripgrep`, `tree`, `unzip`, `wget`, `zstd` |
61
+ | `ubuntu` | `apt` | `docker.io/library/ubuntu:latest` | `build-essential`, `ca-certificates`, `curl`, `diffutils`, `fd-find`, `git`, `jq`, `less`, `netcat-openbsd`, `openssh-client`, `patch`, `procps`, `python3`, `ripgrep`, `tree`, `unzip`, `wget`, `zstd` |
62
+ | `alpine` | `apk` | `docker.io/library/alpine:latest` | `bash`, `build-base`, `ca-certificates`, `curl`, `diffutils`, `fd`, `git`, `jq`, `less`, `openssh-client`, `patch`, `procps`, `python3`, `ripgrep`, `tree`, `unzip`, `wget`, `zstd` |
63
+
52
64
  ## Tools
53
65
 
54
66
  The plugin registers the following model-facing tools. Tools marked `✱` require
package/docs/usage.zh.md CHANGED
@@ -42,6 +42,17 @@ dsh-podman 将容器运行的镜像组织为三个层级:
42
42
  镜像引用(`imageId`、`parent`、`image`)**只能是短名称**(没有镜像仓库前缀,没有
43
43
  `:tag`)。
44
44
 
45
+ ### 基础镜像
46
+
47
+ 每个基础镜像都从其原始引用构建,安装同一份由 dsh-podman
48
+ 拥有的固定软件包列表;自定义镜像继承其父镜像的软件包管理器。
49
+
50
+ | 短名称 | 软件包管理器 | 原始镜像 | 软件包 |
51
+ | ----------- | ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `archlinux` | `pacman` | `docker.io/library/archlinux:latest` | `base-devel`、`ca-certificates`、`curl`、`diffutils`、`fd`、`git`、`inetutils`、`jq`、`less`、`openbsd-netcat`、`openssh`、`patch`、`procps-ng`、`python`、`ripgrep`、`tree`、`unzip`、`wget`、`zstd` |
53
+ | `ubuntu` | `apt` | `docker.io/library/ubuntu:latest` | `build-essential`、`ca-certificates`、`curl`、`diffutils`、`fd-find`、`git`、`jq`、`less`、`netcat-openbsd`、`openssh-client`、`patch`、`procps`、`python3`、`ripgrep`、`tree`、`unzip`、`wget`、`zstd` |
54
+ | `alpine` | `apk` | `docker.io/library/alpine:latest` | `bash`、`build-base`、`ca-certificates`、`curl`、`diffutils`、`fd`、`git`、`jq`、`less`、`openssh-client`、`patch`、`procps`、`python3`、`ripgrep`、`tree`、`unzip`、`wget`、`zstd` |
55
+
45
56
  ## 工具
46
57
 
47
58
  插件注册了以下面向模型的工具。标记 `✱`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exagone313/dsh-podman",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Podman-backed execution for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "repository": {