dsh-session-bridge 0.3.1 → 0.3.2-alpha.1

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 CHANGED
@@ -119,13 +119,22 @@ time — not just its final reply — and act on it:
119
119
  # type-check + bundle the host build + pack a tgz (DSH_CHECKOUT points at the dsh source checkout)
120
120
  bash scripts/build.sh && npm run build:client
121
121
 
122
+ # type-check src/ against the dsh that is actually installed (no checkout needed)
123
+ npm run check:compat
124
+
122
125
  # via the injector toolchain
123
126
  dev_build_plugin dsh-session-bridge
124
127
  ```
125
128
 
126
129
  `build.sh` type-links against a local DSH checkout and is only for local dev.
130
+ That checkout frequently lags the harness the plugin is loaded into, so a green
131
+ `build.sh` does **not** prove the plugin works on the running DSH — `build.sh`
132
+ warns when the two versions differ. Use `npm run check:compat` for that: it
133
+ type-checks `src/` against the `lib/types/*.d.ts` shipped inside the installed
134
+ DSH package, which is the exact API surface the plugin loads against.
135
+
127
136
  The GitHub Actions CI (`ci.yml`) instead resolves the `@deepseek-ai/dsh-*`
128
- prereleases from the registry — pinned to the `0.1.2-alpha.2` line, which is
137
+ prereleases from the registry — pinned to the `0.1.6-alpha.1` line, which is
129
138
  the DSH API surface this code targets — then runs `pnpm typecheck` and
130
139
  `pnpm build:client` (the self-contained `tsdown` bundle). Bump that pin
131
140
  together with the code when you migrate to a newer DSH API.
@@ -149,6 +158,14 @@ publishing.
149
158
  npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-session-bridge
150
159
  ```
151
160
 
161
+ A prerelease tag (`v0.3.2-alpha.1`) is published to its own dist-tag (`alpha`,
162
+ `beta`, `rc`) instead of `latest`, so it cannot displace the stable release for
163
+ other users. Opt in explicitly:
164
+
165
+ ```bash
166
+ npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-session-bridge@alpha
167
+ ```
168
+
152
169
  pnpm installs the published tarball and runs its `prepare` script (`tsdown`) to
153
170
  ensure `lib/` is present, then `dsh` activates the bundle.
154
171
 
package/README.zh.md CHANGED
@@ -87,10 +87,19 @@
87
87
  # 类型检查 + 打包 host bundle + 生成 tgz(DSH_CHECKOUT 指向 dsh 源码 checkout)
88
88
  bash scripts/build.sh && npm run build:client
89
89
 
90
+ # 针对"实际安装的 dsh"做类型检查(不需要 checkout)
91
+ npm run check:compat
92
+
90
93
  # 或经注入器工具链
91
94
  dev_build_plugin dsh-session-bridge
92
95
  ```
93
96
 
97
+ `build.sh` 按本地 dsh 源码 checkout 做类型链接,仅用于本地开发。该 checkout 常常
98
+ 落后于插件实际加载进的 harness,因此 `build.sh` 通过**并不**代表插件在运行中的
99
+ DSH 上可用——两者版本不一致时 `build.sh` 会给出警告。要验证运行版本请用
100
+ `npm run check:compat`:它按已安装 DSH 包内随附的 `lib/types/*.d.ts`(即插件真正
101
+ 加载的 API 面)对 `src/` 做类型检查。
102
+
94
103
  ## 部署
95
104
 
96
105
  DSH web 从活动 profile 加载外部插件。本包是一个 **bundle**:`package.json` 声明了
@@ -107,6 +116,13 @@ DSH web 从活动 profile 加载外部插件。本包是一个 **bundle**:`pac
107
116
  npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-session-bridge
108
117
  ```
109
118
 
119
+ 预发布标签(`v0.3.2-alpha.1`)会发布到自己的 dist-tag(`alpha`/`beta`/`rc`),
120
+ 而不会占用 `latest`,因此不会顶掉其他用户使用的稳定版。需要显式选用:
121
+
122
+ ```bash
123
+ npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-session-bridge@alpha
124
+ ```
125
+
110
126
  pnpm 会安装发布的 tarball 并运行其 `prepare` 脚本(`tsdown`)以确保 `lib/` 就绪,
111
127
  随后 `dsh` 激活该 bundle。
112
128
 
package/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-session-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.2-alpha.1",
4
4
  "main": "./lib/index.js",
5
5
  "description": "会话桥:创建主会话 / 向任意会话发消息 / 等待回复 / 读取消息 / 按名或 id 查找会话(支持跨工作区);另含监控/调度主任务与归档会话。",
6
6
  "engines": {
7
- "dsh": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0"
7
+ "dsh": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0"
8
8
  },
9
9
  "contributes": {
10
10
  "tools": [
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { appendFileSync, mkdirSync } from "node:fs";
6
6
  import { randomUUID } from "node:crypto";
7
- //#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
7
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
8
8
  /**
9
9
  * Shared filesystem path helpers for DeepSeek Harness user data.
10
10
  *
@@ -2200,7 +2200,7 @@ var Service = class Service {
2200
2200
  }
2201
2201
  };
2202
2202
  //#endregion
2203
- //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
2203
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
2204
2204
  /** The one Remote failure class shared by owners, the Gateway, and consumers. */
2205
2205
  /**
2206
2206
  * One Remote call failure: a real Error carrying its stable code and typed
@@ -2345,7 +2345,7 @@ function validateName(subject, value) {
2345
2345
  if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
2346
2346
  }
2347
2347
  //#endregion
2348
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
2348
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
2349
2349
  /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
2350
2350
  /**
2351
2351
  * Mark an unreachable closed-union branch.
@@ -2557,7 +2557,7 @@ function deepFreeze(value) {
2557
2557
  return value;
2558
2558
  }
2559
2559
  //#endregion
2560
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
2560
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
2561
2561
  /**
2562
2562
  * Random v4 UUID, minted from `crypto.getRandomValues`.
2563
2563
  * @returns the UUID string.
@@ -2570,7 +2570,7 @@ function randomUUID$1() {
2570
2570
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2571
2571
  }
2572
2572
  //#endregion
2573
- //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2573
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2574
2574
  /**
2575
2575
  * Duplicate-install-safe nominal primitive helpers.
2576
2576
  *
@@ -3187,11 +3187,11 @@ defineMethod("transform", [
3187
3187
  "preserve"
3188
3188
  ], ({ inner }, isInner) => inner.toString(isInner));
3189
3189
  //#endregion
3190
- //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3190
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3191
3191
  /** Largest delay Node schedules without clamping it to one millisecond. */
3192
3192
  const MAX_TIMER_DELAY_MS = 2147483647;
3193
3193
  //#endregion
3194
- //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3194
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3195
3195
  /**
3196
3196
  * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
3197
3197
  * @param summary - the producer's one-line account, of any length.
@@ -3441,13 +3441,15 @@ function failureSnapshot(value) {
3441
3441
  const status = candidate.status;
3442
3442
  const providerRetryAfterMs = candidate.providerRetryAfterMs;
3443
3443
  const requestId = candidate.requestId;
3444
- if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0)) return void 0;
3444
+ const offloadImages = candidate.offloadImages;
3445
+ if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
3445
3446
  return Object.freeze({
3446
3447
  message,
3447
3448
  code,
3448
3449
  ...status === void 0 ? {} : { status },
3449
3450
  ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
3450
- ...requestId === void 0 ? {} : { requestId }
3451
+ ...requestId === void 0 ? {} : { requestId },
3452
+ ...offloadImages === void 0 ? {} : { offloadImages }
3451
3453
  });
3452
3454
  } catch (_sdkFailureGetter) {
3453
3455
  return;
@@ -3686,7 +3688,8 @@ var LlmError = class extends HarnessError {
3686
3688
  code,
3687
3689
  ...options?.status === void 0 ? {} : { status: options.status },
3688
3690
  ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
3689
- ...options?.requestId === void 0 ? {} : { requestId: options.requestId }
3691
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
3692
+ ...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
3690
3693
  });
3691
3694
  }
3692
3695
  };
@@ -9077,7 +9080,7 @@ function superRefine(fn, params) {
9077
9080
  return /* @__PURE__ */ _superRefine(fn, params);
9078
9081
  }
9079
9082
  //#endregion
9080
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
9083
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
9081
9084
  /**
9082
9085
  * Backend-facing vocabulary of the storage hub: a backend owns one medium
9083
9086
  * (a file-tree root, a database file) and exposes operation groups over it.
@@ -9088,7 +9091,7 @@ function superRefine(fn, params) {
9088
9091
  /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
9089
9092
  const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
9090
9093
  //#endregion
9091
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_8f094eabec231701d08ef59df76cca5d/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
9094
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_404bb44176e435660710548cd6a4e0aa/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
9092
9095
  /**
9093
9096
  * Domain declaration vocabulary. A spec object is the single source of a
9094
9097
  * domain's identity, layout, and record schemas: the owning package defines
@@ -10038,7 +10041,7 @@ var SessionMonitor = class {
10038
10041
  }
10039
10042
  };
10040
10043
  //#endregion
10041
- //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_a35c65afc1061b4908fad5a531841731/node_modules/@deepseek-ai/dsh-scope/lib/index.js
10044
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_eaf7046112a9b60866fe7f69bc8f6f64/node_modules/@deepseek-ai/dsh-scope/lib/index.js
10042
10045
  /**
10043
10046
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
10044
10047
  *
@@ -10317,7 +10320,92 @@ function scopeTarget(base, key) {
10317
10320
  return carrier;
10318
10321
  }
10319
10322
  //#endregion
10320
- //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.5-alpha.2_2b754afda902b21956a20e8a701ed6ef/node_modules/@deepseek-ai/dsh-tools/lib/index.js
10323
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-llm@0_1d3a97f96d2766469566ca2b01247bfa/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
10324
+ /**
10325
+ * The escalation vocabulary and choreography shared by every sandbox-enforcing
10326
+ * tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the
10327
+ * strictly-wider ladder, the argument-pairing validation, the model-facing
10328
+ * denial/hint markers, and {@link approveEscalation} — the ordered fail-closed
10329
+ * sequence that resolves a `sandbox_permissions` request through a
10330
+ * user-approval channel BEFORE anything executes. One home keeps the two
10331
+ * families' approval ordering and verbatim error texts from drifting apart.
10332
+ *
10333
+ * The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}),
10334
+ * not the approval service type: the tool layer — which owns the agent, the
10335
+ * call id, and the tool name — closes over `ctx.approval.request(...)` and
10336
+ * hands the closure down, so this package never depends on the approval or
10337
+ * agent packages.
10338
+ *
10339
+ * @module dsh-sandbox/escalation
10340
+ */
10341
+ /**
10342
+ * The strictly-wider table: what a call whose effective mode is the key may
10343
+ * escalate TO. Checked at EXECUTION, never baked into a tool schema — the
10344
+ * schema's enum is {@link ESCALATION_TARGETS}, because schemas are
10345
+ * registry-global while the effective mode is per-call truth.
10346
+ */
10347
+ const WIDER_MODES = {
10348
+ "read-only": ["workspace-write", "danger-full-access"],
10349
+ "workspace-write": ["danger-full-access"]
10350
+ };
10351
+ /**
10352
+ * The closed escalation-target vocabulary — every mode a call could ever
10353
+ * escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
10354
+ * whenever the mounted capability confines: cutting the enum down to the modes
10355
+ * wider than the composition's DEFAULT would strand a session whose effective
10356
+ * mode sits below it (a `danger-full-access` default would advertise nothing
10357
+ * while a narrower-switched session stays confined with no lever).
10358
+ */
10359
+ const ESCALATION_TARGETS = ["workspace-write", "danger-full-access"];
10360
+ /**
10361
+ * Validate the escalation argument pairing a tool schema cannot express:
10362
+ * `sandbox_permissions` and `justification` travel together — an approval
10363
+ * prompt without a reason, or a reason driving nothing, is a malformed ask —
10364
+ * and the justification must be a non-empty sentence.
10365
+ * @param sandboxPermissions - the raw `sandbox_permissions` argument, if given.
10366
+ * @param justification - the raw `justification` argument, if given.
10367
+ */
10368
+ function validateEscalationArgs(sandboxPermissions, justification) {
10369
+ if (sandboxPermissions !== void 0 && justification === void 0) throw new Error("invalid escalation: sandbox_permissions requires a justification");
10370
+ if (justification !== void 0 && sandboxPermissions === void 0) throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
10371
+ if (justification !== void 0 && justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
10372
+ }
10373
+ /**
10374
+ * Resolve a sandbox-escalation request BEFORE anything executes: check strict
10375
+ * widening against the call's effective mode, then resolve the approval
10376
+ * channel, then map every outcome — the ordered fail-closed sequence both
10377
+ * enforcing families share. Returns the granted mode to stamp onto exactly
10378
+ * this call; throws the distinct verbatim text for every other path (a
10379
+ * non-widening request, a missing approval service, an agent-less execution,
10380
+ * a rejection, a cancellation, an unanswerable ask) — the tool registry turns
10381
+ * the throw into the call's isError result, and nothing has run. A
10382
+ * non-widening request never prompts a human.
10383
+ * @param request - the escalation to judge (see {@link EscalationRequest}).
10384
+ * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
10385
+ * @returns the granted mode, consumed by the one call that asked.
10386
+ */
10387
+ async function approveEscalation(request, approval) {
10388
+ const { requestedMode: mode, effectiveMode, justification, subject } = request;
10389
+ if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode)) throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
10390
+ if (approval.approver === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`);
10391
+ if (approval.agent === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`);
10392
+ const outcome = await approval.approver.request({
10393
+ agent: approval.agent,
10394
+ toolName: approval.toolName,
10395
+ callId: approval.callId,
10396
+ reason: `escalate sandbox to ${mode}: ${justification}`,
10397
+ ...approval.signal ? { signal: approval.signal } : {}
10398
+ });
10399
+ switch (outcome) {
10400
+ case "allowed-once": return mode;
10401
+ case "rejected": throw new Error(`the user rejected escalating this ${subject} to "${mode}"`);
10402
+ case "cancelled": throw new Error(`approval for escalating to "${mode}" was cancelled`);
10403
+ case "unavailable": throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`);
10404
+ default: return assertNever(outcome, "EscalationOutcome");
10405
+ }
10406
+ }
10407
+ //#endregion
10408
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.6-alpha.1_e89197212305e1b19bd6df05221bc44b/node_modules/@deepseek-ai/dsh-tools/lib/index.js
10321
10409
  /**
10322
10410
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
10323
10411
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -11188,7 +11276,7 @@ const TYPESCRIPT_FLAVOR = {
11188
11276
  description: "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.",
11189
11277
  codeDescription: "The program: the body of an async TypeScript function."
11190
11278
  };
11191
- /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link CodeSdkLanguage}. */
11279
+ /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link PtcSdkLanguage}. */
11192
11280
  const RUN_CODE_FLAVORS = {
11193
11281
  typescript: TYPESCRIPT_FLAVOR,
11194
11282
  python: {
@@ -11203,17 +11291,48 @@ const RUN_CODE_FLAVORS = {
11203
11291
  * can never drift.
11204
11292
  */
11205
11293
  const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION = "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\".";
11294
+ const RUN_CODE_CONTROLS = {
11295
+ timeoutMs: {
11296
+ type: "number",
11297
+ description: "Positive elapsed-time budget in milliseconds, capped by the deployment maximum."
11298
+ },
11299
+ sandbox_permissions: {
11300
+ type: "string",
11301
+ enum: [...ESCALATION_TARGETS],
11302
+ description: "Wider sandbox mode for this complete program execution; requires justification and approval."
11303
+ },
11304
+ justification: {
11305
+ type: "string",
11306
+ description: "Reason this complete program needs wider access, shown to the user for approval."
11307
+ }
11308
+ };
11309
+ function controlParameters(runtime) {
11310
+ if (runtime === void 0) return RUN_CODE_CONTROLS;
11311
+ return {
11312
+ ...runtime.timeout === void 0 ? {} : { timeoutMs: {
11313
+ ...RUN_CODE_CONTROLS.timeoutMs,
11314
+ description: `Positive elapsed-time budget in milliseconds, including nested tool and approval waits. Default ${runtime.timeout.defaultMs}; capped at ${runtime.timeout.maxMs}. Zero does not disable the deadline.`
11315
+ } },
11316
+ ...runtime.sandboxMode === void 0 ? {} : {
11317
+ sandbox_permissions: RUN_CODE_CONTROLS.sandbox_permissions,
11318
+ justification: RUN_CODE_CONTROLS.justification
11319
+ }
11320
+ };
11321
+ }
11322
+ function escalationGuidance(runtime) {
11323
+ return runtime?.sandboxMode === void 0 ? "" : " A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.";
11324
+ }
11206
11325
  /**
11207
11326
  * Resolve the {@link RunCodeFlavor} for the loaded runtime's language, read at
11208
11327
  * schema-emission time so the model-visible `run_code` schema always matches
11209
11328
  * the SDK section's language. `peekRuntime` returns `undefined` only when no
11210
11329
  * runtime is mounted, which reaches this function through definition readers
11211
11330
  * and `schemas()` — the doc-catalog harvest is the only shipped one, and none
11212
- * of them feeds a model, because `wireSchemas` calls `requireCodeRuntime`
11331
+ * of them feeds a model, because `wireSchemas` calls `requirePtcRuntime`
11213
11332
  * before projecting — so that path degrades to {@link TYPESCRIPT_FLAVOR}. A
11214
11333
  * mounted runtime whose language has no flavor entry fails loud, exactly as
11215
- * `requireCodeRuntime` rejects it at assembly. Keeping this table in step with
11216
- * `SDK_RENDERERS` is the compiler's job ({@link CodeSdkLanguage}); what this
11334
+ * `requirePtcRuntime` rejects it at assembly. Keeping this table in step with
11335
+ * `SDK_RENDERERS` is the compiler's job ({@link PtcSdkLanguage}); what this
11217
11336
  * guard owns is the runtime-supplied language neither table knows, which never
11218
11337
  * yields a wrong-language schema for a real runtime.
11219
11338
  */
@@ -11382,7 +11501,8 @@ function createRunCodeTool(registry, options) {
11382
11501
  type: "string",
11383
11502
  required: true,
11384
11503
  description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
11385
- }
11504
+ },
11505
+ ...RUN_CODE_CONTROLS
11386
11506
  },
11387
11507
  output: {
11388
11508
  schema: {
@@ -11394,12 +11514,37 @@ function createRunCodeTool(registry, options) {
11394
11514
  required: true,
11395
11515
  items: { type: "string" }
11396
11516
  },
11397
- result: { type: "json" }
11517
+ result: { type: "json" },
11518
+ sandbox: {
11519
+ type: "object",
11520
+ additionalProperties: false,
11521
+ properties: {
11522
+ mode: {
11523
+ type: "string",
11524
+ required: true,
11525
+ enum: [
11526
+ "read-only",
11527
+ "workspace-write",
11528
+ "danger-full-access"
11529
+ ]
11530
+ },
11531
+ denied: {
11532
+ type: "boolean",
11533
+ required: true
11534
+ },
11535
+ enforcement: {
11536
+ type: "string",
11537
+ enum: ["full", "partial"]
11538
+ }
11539
+ }
11540
+ }
11398
11541
  }
11399
11542
  },
11400
11543
  render: (_args, value) => {
11401
11544
  const rendered = value.result === void 0 ? "" : renderValue(value.result);
11402
11545
  const parts = [value.logs.join("\n"), rendered].filter((part) => part.length > 0);
11546
+ if (value.sandbox?.enforcement === "partial") parts.push("File sandbox enforcement is partial on this host.");
11547
+ if (value.sandbox?.denied) parts.push(`The ${value.sandbox.mode} file sandbox denied an operation.${escalationGuidance(peekRuntime())}`);
11403
11548
  return [{
11404
11549
  type: "text",
11405
11550
  text: parts.length > 0 ? parts.join("\n") : "(run_code completed with no output)"
@@ -11409,6 +11554,31 @@ function createRunCodeTool(registry, options) {
11409
11554
  async execute(args, exec) {
11410
11555
  if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
11411
11556
  const runtime = requireRuntime();
11557
+ validateEscalationArgs(args.sandbox_permissions, args.justification);
11558
+ if (args.timeoutMs !== void 0 && runtime.timeout === void 0) throw new Error("timeoutMs is not available for this PTC runtime");
11559
+ if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) throw new Error("invalid timeoutMs: expected a positive finite number");
11560
+ const standingPolicy = runtime.sandboxMode === void 0 ? void 0 : options.resolveSandboxPolicy(exec);
11561
+ let policy = standingPolicy;
11562
+ if (args.sandbox_permissions !== void 0 && args.justification !== void 0) {
11563
+ if (standingPolicy === void 0) throw new Error("sandbox_permissions is not available for this PTC runtime");
11564
+ const approvedMode = await approveEscalation({
11565
+ requestedMode: args.sandbox_permissions,
11566
+ justification: args.justification,
11567
+ effectiveMode: standingPolicy.mode,
11568
+ subject: "program"
11569
+ }, {
11570
+ approver: options.peekApprover(),
11571
+ agent: exec.agent,
11572
+ callId: exec.callId,
11573
+ toolName: RUN_CODE_NAME,
11574
+ signal: exec.signal
11575
+ });
11576
+ policy = {
11577
+ ...standingPolicy,
11578
+ mode: approvedMode
11579
+ };
11580
+ }
11581
+ exec.signal.throwIfAborted();
11412
11582
  const runController = new AbortController();
11413
11583
  const onOuterAbort = () => {
11414
11584
  runController.abort(exec.signal.reason);
@@ -11490,7 +11660,8 @@ function createRunCodeTool(registry, options) {
11490
11660
  while (logWork.size > 0) await Promise.allSettled([...logWork]);
11491
11661
  };
11492
11662
  const runOver = () => runController.signal.aborted;
11493
- const binding = (name) => async (rawArgs) => {
11663
+ const binding = (schema) => async (rawArgs) => {
11664
+ const { name } = schema;
11494
11665
  if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
11495
11666
  const normalized = jsonNormalizeArgs(rawArgs);
11496
11667
  const n = ++dispatches;
@@ -11499,6 +11670,7 @@ function createRunCodeTool(registry, options) {
11499
11670
  callId: subCallId,
11500
11671
  rootCallId: exec.rootCallId,
11501
11672
  name,
11673
+ schema,
11502
11674
  arguments: normalized.dispatched,
11503
11675
  ...exec.agent ? { agent: exec.agent } : {},
11504
11676
  parent: exec.token,
@@ -11533,6 +11705,7 @@ function createRunCodeTool(registry, options) {
11533
11705
  name,
11534
11706
  arguments: normalized.logged,
11535
11707
  isError: result.isError,
11708
+ ...result.error?.info === void 0 ? {} : { error: result.error.info },
11536
11709
  content: logged
11537
11710
  });
11538
11711
  })().finally(() => {
@@ -11603,13 +11776,13 @@ function createRunCodeTool(registry, options) {
11603
11776
  if (schema.name === "run_code") continue;
11604
11777
  Object.defineProperty(functions, schema.name, {
11605
11778
  enumerable: true,
11606
- value: binding(schema.name)
11779
+ value: binding(deepFreeze(schema))
11607
11780
  });
11608
11781
  }
11609
11782
  try {
11610
11783
  let result;
11611
11784
  try {
11612
- result = await runtime.run({
11785
+ result = await runtime.run(runtime.resolve({
11613
11786
  program: args.code,
11614
11787
  bindings: [{
11615
11788
  global: "tools",
@@ -11619,18 +11792,23 @@ function createRunCodeTool(registry, options) {
11619
11792
  memberNameProperty: "toolName"
11620
11793
  }
11621
11794
  }],
11622
- signal: runController.signal
11623
- });
11795
+ signal: runController.signal,
11796
+ ...exec.agent?.session.header.cwd !== void 0 ? { cwd: exec.agent.session.header.cwd } : {},
11797
+ ...policy !== void 0 ? { sandboxPolicy: policy } : {},
11798
+ ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {}
11799
+ }));
11624
11800
  } finally {
11625
11801
  runController.abort("run_code settled");
11626
11802
  await drainDispatches();
11627
11803
  }
11628
11804
  if (result.error) {
11629
11805
  const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join("\n")}` : "";
11630
- throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`);
11806
+ const sandboxText = result.sandbox === void 0 ? "" : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === void 0 ? "" : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? "; operation denied" : ""}.`;
11807
+ throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime) : ""}`);
11631
11808
  }
11632
11809
  return {
11633
11810
  logs: result.logs,
11811
+ ...result.sandbox === void 0 ? {} : { sandbox: result.sandbox },
11634
11812
  ...result.value !== void 0 ? { result: result.value } : {}
11635
11813
  };
11636
11814
  } finally {
@@ -11646,7 +11824,11 @@ function createRunCodeTool(registry, options) {
11646
11824
  });
11647
11825
  Object.defineProperty(definition, "description", {
11648
11826
  enumerable: true,
11649
- get: () => resolveFlavor(peekRuntime).description
11827
+ get: () => {
11828
+ const runtime = peekRuntime();
11829
+ const instructions = runtime?.executionInstructions;
11830
+ return resolveFlavor(peekRuntime).description + (instructions ? ` ${instructions}` : "") + (runtime === void 0 ? "" : " The working directory is the Session's current directory.") + escalationGuidance(runtime);
11831
+ }
11650
11832
  });
11651
11833
  Object.defineProperty(definition, "parameters", {
11652
11834
  enumerable: true,
@@ -11660,7 +11842,8 @@ function createRunCodeTool(registry, options) {
11660
11842
  type: "string",
11661
11843
  required: true,
11662
11844
  description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
11663
- }
11845
+ },
11846
+ ...controlParameters(peekRuntime())
11664
11847
  })
11665
11848
  });
11666
11849
  return definition;
@@ -11945,7 +12128,7 @@ function renderToolsSdk(schemas) {
11945
12128
  * PTC mode codegen — Python flavor. The pure projection from registered tool schemas to the
11946
12129
  * Python SDK text the model programs against under `runtime.language === 'python'`. Sibling of
11947
12130
  * {@link ./ts-types.ts | ts-types.ts}; the two files are two projections of the same registry
11948
- * store, keyed by the loaded {@link @deepseek-ai/dsh-code-runtime#CodeRuntime.language | code
12131
+ * store, keyed by the loaded {@link @deepseek-ai/dsh-ptc-runtime#PtcRuntime.language | PTC
11949
12132
  * runtime's language}.
11950
12133
  *
11951
12134
  * Under `mode: 'ptc'` the native tool schemas are omitted from the request, so this generated
@@ -12632,17 +12815,17 @@ function renderToolsSdkPy(schemas) {
12632
12815
  */
12633
12816
  /**
12634
12817
  * Language → SDK-section renderer. The registry looks up the loaded
12635
- * `ctx.codeRuntime.language` in this table when assembling the `tools:sdk`
12818
+ * `ctx.ptcRuntime.language` in this table when assembling the `tools:sdk`
12636
12819
  * section under a non-native mode; a runtime whose language is not a key
12637
12820
  * fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
12638
- * new backend language is three parallel edits — a {@link CodeSdkLanguage}
12821
+ * new backend language is three parallel edits — a {@link PtcSdkLanguage}
12639
12822
  * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `ptc.ts` for
12640
12823
  * its `run_code` schema strings — plus the renderer function this table points
12641
12824
  * at. The `satisfies` clause pins this table's key set to that union, which
12642
12825
  * the flavor table is checked against too, so any of the three left out is a
12643
12826
  * typecheck failure. What no check reaches is the prose that names the values
12644
- * instead of deriving them: the seam's `dsh-code-runtime` README pair, its
12645
- * `CodeRuntime.language` JSDoc, and `docs/subsystems/code-runtime.md`
12827
+ * instead of deriving them: the seam's `dsh-ptc-runtime` README pair, its
12828
+ * `PtcRuntime.language` JSDoc, and `docs/subsystems/ptc-runtime.md`
12646
12829
  * with its zh pair, plus this package's own README pair and the
12647
12830
  * {@link Config.mode} JSDoc.
12648
12831
  */
@@ -12881,12 +13064,13 @@ var ToolRuntime = class extends Service {
12881
13064
  return {
12882
13065
  name: "tools:sdk",
12883
13066
  order: this.ctx.systemPrompt.getSectionOrder("TOOLS_SDK"),
13067
+ interpolate: false,
12884
13068
  text: (context) => {
12885
13069
  const mode = this.modeFor(context.scope);
12886
13070
  if (mode === "native") return "";
12887
- const runtime = this.requireCodeRuntime(mode);
13071
+ const runtime = this.requirePtcRuntime(mode);
12888
13072
  const render = SDK_RENDERERS[runtime.language];
12889
- /* v8 ignore next -- requireCodeRuntime rejects an unknown language before this runs. */
13073
+ /* v8 ignore next -- requirePtcRuntime rejects an unknown language before this runs. */
12890
13074
  if (render === void 0) throw new Error(`dsh-tools: no SDK renderer for ${runtime.language}`);
12891
13075
  return render(this.sdkSchemas(context.scope));
12892
13076
  }
@@ -12915,10 +13099,16 @@ var ToolRuntime = class extends Service {
12915
13099
  * and only for scopes whose mode actually presents it.
12916
13100
  * @returns the shared transport definition.
12917
13101
  */
12918
- requireCodeTransport() {
13102
+ requirePtcTransport() {
12919
13103
  this.ptcTransport ??= createRunCodeTool(this, {
12920
- requireRuntime: () => this.requireCodeRuntime(this.defaultMode),
12921
- peekRuntime: () => this.ctx.get("codeRuntime"),
13104
+ requireRuntime: () => this.requirePtcRuntime(this.defaultMode),
13105
+ peekApprover: () => this.ctx.get("approval"),
13106
+ resolveSandboxPolicy: (exec) => {
13107
+ const policy = this.ctx.get("sandboxPolicy");
13108
+ if (policy === void 0) throw new Error("dsh-tools: confined PTC runtime requires sandboxPolicy");
13109
+ return policy.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
13110
+ },
13111
+ peekRuntime: () => this.ctx.get("ptcRuntime"),
12922
13112
  maxParallel: this.maxParallelSubCalls,
12923
13113
  shapeDispatchLog: (dispatch) => this.shapeDispatchLog(dispatch)
12924
13114
  });
@@ -12963,7 +13153,7 @@ var ToolRuntime = class extends Service {
12963
13153
  schemas: [...view.visible.values()].map((definition) => this.schemaOf(definition, false)),
12964
13154
  knownNames: [...view.knownNames]
12965
13155
  };
12966
- this.requireCodeRuntime(mode);
13156
+ this.requirePtcRuntime(mode);
12967
13157
  const schemas = [...view.visible.values()].map((definition) => this.schemaOf(definition, false));
12968
13158
  if (mode === "ptc") return {
12969
13159
  schemas: schemas.filter((schema) => schema.name === RUN_CODE_NAME),
@@ -12975,10 +13165,10 @@ var ToolRuntime = class extends Service {
12975
13165
  };
12976
13166
  }
12977
13167
  /**
12978
- * Resolve the code runtime or throw the actionable misconfiguration error.
13168
+ * Resolve the PTC runtime or throw the actionable misconfiguration error.
12979
13169
  * Read at use time (assembly / run_code execution), NOT via static
12980
13170
  * `inject`: an inject entry would hold `ctx.tools` — and every tool plugin
12981
- * behind it — hostage to a code runtime existing even under `mode:
13171
+ * behind it — hostage to a PTC runtime existing even under `mode:
12982
13172
  * 'native'`.
12983
13173
  *
12984
13174
  * Assembly and `run_code` execution read separately, so the language is not
@@ -12988,9 +13178,9 @@ var ToolRuntime = class extends Service {
12988
13178
  * other. Binding it is deferred until a second backend ships (the first
12989
13179
  * point it is testable).
12990
13180
  */
12991
- requireCodeRuntime(mode) {
12992
- const runtime = this.ctx.get("codeRuntime");
12993
- if (!runtime) throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker-thread) or set tools mode to "native"`);
13181
+ requirePtcRuntime(mode) {
13182
+ const runtime = this.ctx.get("ptcRuntime");
13183
+ if (!runtime) throw new Error(`dsh-tools: mode "${mode}" requires a PTC runtime — load a ctx.ptcRuntime implementation (e.g. @deepseek-ai/dsh-ptc-runtime-node) or set tools mode to "native"`);
12994
13184
  if (!Object.hasOwn(SDK_RENDERERS, runtime.language)) {
12995
13185
  const known = Object.keys(SDK_RENDERERS).map((name) => JSON.stringify(name)).join(", ");
12996
13186
  throw new Error(`dsh-tools: no SDK renderer registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
@@ -13104,7 +13294,7 @@ var ToolRuntime = class extends Service {
13104
13294
  knownNames.add(name);
13105
13295
  visible.set(name, definition);
13106
13296
  }
13107
- if (this.modeFor(scope) !== "native") visible.set(RUN_CODE_NAME, this.requireCodeTransport());
13297
+ if (this.modeFor(scope) !== "native") visible.set(RUN_CODE_NAME, this.requirePtcTransport());
13108
13298
  return {
13109
13299
  visible,
13110
13300
  knownNames,
@@ -13275,6 +13465,7 @@ var ToolRuntime = class extends Service {
13275
13465
  signal,
13276
13466
  ...agent !== void 0 ? { agent } : {},
13277
13467
  ...parent !== void 0 ? { parent } : {},
13468
+ ...exec.schema !== void 0 ? { schema: exec.schema } : {},
13278
13469
  deferContext(context) {
13279
13470
  deferredContexts.push(context);
13280
13471
  },
@@ -13357,7 +13548,13 @@ var ToolRuntime = class extends Service {
13357
13548
  exec,
13358
13549
  result: toolAbortedBeforeDispatchResult()
13359
13550
  });
13551
+ if (decision.kind === "cancel") return await next({
13552
+ kind: "post-result",
13553
+ exec,
13554
+ result: toolAbortedBeforeDispatchResult()
13555
+ });
13360
13556
  const denialReason = decision.kind === "allow" ? this.guardReason(exec) : decision.reason;
13557
+ const denialInfo = decision.kind === "deny" ? decision.info : void 0;
13361
13558
  if (denialReason !== void 0) return await next({
13362
13559
  kind: "post-result",
13363
13560
  exec,
@@ -13367,7 +13564,10 @@ var ToolRuntime = class extends Service {
13367
13564
  text: `Error: ${denialReason}`
13368
13565
  }],
13369
13566
  isError: true,
13370
- error: { message: denialReason }
13567
+ error: {
13568
+ message: denialReason,
13569
+ ...denialInfo === void 0 ? {} : { info: denialInfo }
13570
+ }
13371
13571
  })
13372
13572
  });
13373
13573
  if (this.callerCancelled(exec)) return await next({
@@ -13818,7 +14018,7 @@ function toolAbortedBeforeDispatchResult(prior) {
13818
14018
  };
13819
14019
  }
13820
14020
  //#endregion
13821
- //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.5-alpha.2_cfc1690176e5c008b05d52ad11161b75/node_modules/@deepseek-ai/dsh-agent/lib/index.js
14021
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.6-alpha.1_ce40d13c69e7e1ce48818baba37665e1/node_modules/@deepseek-ai/dsh-agent/lib/index.js
13822
14022
  /**
13823
14023
  * Agent-scoped model selection shared by runtime entry points.
13824
14024
  * @module @deepseek-ai/dsh-agent/model-selection
@@ -14057,8 +14257,10 @@ function registerCreate(env) {
14057
14257
  ...model !== "" ? { model } : {},
14058
14258
  ...reasoningEffort === void 0 ? {} : { reasoningEffort }
14059
14259
  },
14060
- setup: (agentCtx) => {
14061
- if (caller !== void 0) env.ctx.agentPresets.composeFrom(agentCtx, caller.ctx);
14260
+ setup: async (agentCtx) => {
14261
+ const requestedPreset = typeof args.agentPreset === "string" && args.agentPreset.trim() !== "" ? args.agentPreset.trim() : void 0;
14262
+ if (requestedPreset !== void 0) await env.ctx.agentPresets.mount(agentCtx, requestedPreset);
14263
+ else if (caller !== void 0) env.ctx.agentPresets.composeFrom(agentCtx, caller.ctx);
14062
14264
  if (installModelSelection$1 !== void 0) {
14063
14265
  installModelSelection$1(agentCtx, selection);
14064
14266
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.2-alpha.1",
4
4
  "description": "会话桥(dsh-session-bridge):通过提示词创建新的主会话(顶层 UI 会话)、向任意会话发送消息、等待会话的下一条回复、读取会话消息,并按会话名或 id 跨工作区查找会话;此外支持监控/调度主任务与归档会话。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -52,28 +52,28 @@
52
52
  },
53
53
  "license": "MIT",
54
54
  "peerDependencies": {
55
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
56
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
57
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
58
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
59
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
60
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
61
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
62
- "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
55
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
56
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
57
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
58
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
59
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
60
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
61
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
62
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
63
63
  "cordis": "^4.0.0-rc.7"
64
64
  },
65
65
  "devDependencies": {
66
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
67
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
68
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
69
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
70
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
71
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
72
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
73
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
74
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
75
- "@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
76
- "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
66
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
67
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
68
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
69
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
70
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
71
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
72
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
73
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
74
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
75
+ "@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
76
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
77
77
  "@types/node": "^24.0.0",
78
78
  "cordis": "^4.0.0-rc.7",
79
79
  "tsdown": "^0.22.14",
@@ -82,6 +82,7 @@
82
82
  "scripts": {
83
83
  "build": "bash scripts/build.sh",
84
84
  "build:client": "tsdown",
85
+ "check:compat": "node scripts/check-dsh-compat.mjs",
85
86
  "typecheck": "tsc -p tsconfig.json --noEmit"
86
87
  }
87
88
  }
package/scripts/build.sh CHANGED
@@ -14,6 +14,24 @@ if [ -z "$CHECKOUT" ] || [ ! -d "$CHECKOUT/packages" ]; then
14
14
  exit 1
15
15
  fi
16
16
 
17
+ # The type-check below runs against $DSH_CHECKOUT, which commonly lags the dsh
18
+ # that actually loads this plugin — a stale checkout type-checks green while the
19
+ # running harness has already moved on. Warn loudly rather than implying support.
20
+ read_pkg_version() {
21
+ node -e "try{process.stdout.write(String(require(process.argv[1]).version))}catch(e){process.stdout.write('unknown')}" "$1" 2>/dev/null || echo unknown
22
+ }
23
+ CHECKOUT_VERSION="$(read_pkg_version "$CHECKOUT/package.json")"
24
+ INSTALLED_VERSION="unknown"
25
+ GLOBAL_ROOT="$(npm root -g 2>/dev/null | head -n1 || true)"
26
+ if [ -n "$GLOBAL_ROOT" ] && [ -f "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json" ]; then
27
+ INSTALLED_VERSION="$(read_pkg_version "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json")"
28
+ fi
29
+ if [ "$INSTALLED_VERSION" != "unknown" ] && [ "$CHECKOUT_VERSION" != "$INSTALLED_VERSION" ]; then
30
+ echo "build: WARNING — checkout is $CHECKOUT_VERSION but the installed dsh is $INSTALLED_VERSION." >&2
31
+ echo "build: the type-check below therefore does NOT cover the running harness;" >&2
32
+ echo "build: run 'npm run check:compat' to type-check against the installed dsh." >&2
33
+ fi
34
+
17
35
  TSC="$CHECKOUT/node_modules/.bin/tsc"
18
36
  if [ ! -x "$TSC" ]; then
19
37
  echo "build: tsc not found at $TSC" >&2
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Type-check src/ against the DSH packages that are ACTUALLY INSTALLED,
4
+ * rather than against $DSH_CHECKOUT.
5
+ *
6
+ * Why this exists: scripts/build.sh type-checks against a DSH *source* checkout.
7
+ * That checkout routinely lags the harness the plugin is loaded into (e.g. the
8
+ * checkout sat at 0.1.5-alpha.2 while the running harness was 0.1.6-alpha.1), so
9
+ * a green `build.sh` silently proves nothing about the running version. This
10
+ * script reads the `lib/types/*.d.ts` that ship inside the installed DSH npm
11
+ * package, i.e. the exact API surface the plugin loads against — so an upstream
12
+ * break shows up here even when the checkout is stale.
13
+ *
14
+ * Usage:
15
+ * node scripts/check-dsh-compat.mjs [--dsh <path>]
16
+ *
17
+ * <path> may be either the DSH package directory or the `@deepseek-ai` scope
18
+ * directory that holds dsh-session, dsh-agent, ... . When omitted, the installed
19
+ * location is probed (DSH_INSTALLED_MODULES, then `npm root -g`).
20
+ *
21
+ * Exits 0 when the plugin's src/ type-checks against those types, 1 otherwise.
22
+ */
23
+ import { execFileSync, execSync } from 'node:child_process'
24
+ import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
25
+ import { dirname, join, resolve } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
29
+
30
+ /** Read a JSON file, or return undefined when it is absent/unreadable. */
31
+ function readJson(file) {
32
+ try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return undefined }
33
+ }
34
+
35
+ /** True when `dir` looks like the `@deepseek-ai` scope directory. */
36
+ function isScopeDir(dir) {
37
+ return dir !== undefined && existsSync(join(dir, 'dsh-session', 'package.json'))
38
+ }
39
+
40
+ /**
41
+ * Resolve the `@deepseek-ai` scope directory holding the installed DSH packages.
42
+ * Accepts an explicit path (package dir or scope dir) and otherwise probes.
43
+ */
44
+ function resolveScopeDir(argv) {
45
+ const flagAt = argv.indexOf('--dsh')
46
+ const explicit = flagAt !== -1 ? argv[flagAt + 1] : (process.env.DSH_INSTALLED_MODULES ?? undefined)
47
+ const candidates = []
48
+ if (explicit !== undefined && explicit !== '') {
49
+ const base = resolve(explicit)
50
+ // Accept the scope dir itself, the dsh package dir, or a node_modules tree.
51
+ candidates.push(base, join(base, 'node_modules', '@deepseek-ai'), join(base, '@deepseek-ai'))
52
+ }
53
+
54
+ // Anchor 1: the global install root. execSync (string command) rather than
55
+ // execFileSync, because npm is a .cmd shim on Windows that cannot be spawned
56
+ // without a shell.
57
+ let globalRoot
58
+ try {
59
+ globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
60
+ } catch { globalRoot = undefined }
61
+ if (globalRoot !== undefined && globalRoot !== '') {
62
+ // npm layout, then the common "dsh nests its own deps" layout.
63
+ candidates.push(join(globalRoot, '@deepseek-ai'), join(globalRoot, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
64
+ }
65
+
66
+ // Anchor 2: walk up from the `dsh` executable itself. Its path is usually a
67
+ // symlink (scoop/volta), so resolve it first or the walk misses the real tree.
68
+ try {
69
+ const found = execSync(process.platform === 'win32' ? 'where dsh' : 'which dsh', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
70
+ .split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== '')
71
+ for (const line of found) {
72
+ let dir
73
+ try { dir = dirname(realpathSync(line)) } catch { dir = dirname(line) }
74
+ for (let hop = 0; hop < 4 && dir !== dirname(dir); hop++) {
75
+ candidates.push(join(dir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
76
+ candidates.push(join(dir, 'node_modules', '@deepseek-ai'))
77
+ dir = dirname(dir)
78
+ }
79
+ }
80
+ } catch { /* dsh not on PATH; the other anchors decide */ }
81
+
82
+ for (const candidate of candidates) if (isScopeDir(candidate)) return candidate
83
+ return undefined
84
+ }
85
+
86
+ /** Resolve a package's types entry point inside the scope directory. */
87
+ function typesEntry(scope, pkg) {
88
+ const dir = join(scope, pkg)
89
+ const manifest = readJson(join(dir, 'package.json'))
90
+ if (manifest === undefined) return undefined
91
+ const declared = typeof manifest.types === 'string' ? manifest.types : undefined
92
+ const candidates = [
93
+ ...(declared !== undefined ? [join(dir, declared)] : []),
94
+ join(dir, 'lib', 'types', 'index.d.ts'),
95
+ join(dir, 'index.d.ts'),
96
+ ]
97
+ return candidates.find((c) => existsSync(c))
98
+ }
99
+
100
+ function fail(message) {
101
+ console.error('check-dsh-compat: ' + message)
102
+ process.exit(1)
103
+ }
104
+
105
+ const scope = resolveScopeDir(process.argv.slice(2))
106
+ if (scope === undefined) {
107
+ fail('could not locate the installed DSH packages. Pass --dsh <path> (the DSH package dir or its @deepseek-ai scope dir) or set DSH_INSTALLED_MODULES.')
108
+ }
109
+
110
+ /** Read the version of the `dsh` package that owns this scope directory. */
111
+ function dshVersionOf(scopeDir) {
112
+ // Nested layout puts dsh two levels above its own scope dir; a flat global
113
+ // install puts it beside the other packages.
114
+ for (const candidate of [join(scopeDir, 'dsh', 'package.json'), join(scopeDir, '..', '..', 'package.json')]) {
115
+ const manifest = readJson(candidate)
116
+ if (manifest?.name === '@deepseek-ai/dsh' && typeof manifest.version === 'string') return manifest.version
117
+ }
118
+ return undefined
119
+ }
120
+
121
+ const dshVersion = dshVersionOf(scope) ?? '(unknown)'
122
+
123
+ // Every DSH package src/ imports, plus the plain `cordis` specifier that
124
+ // package.json declares as a peer.
125
+ const manifest = readJson(join(ROOT, 'package.json')) ?? {}
126
+ const declared = [...Object.keys(manifest.peerDependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
127
+ const wanted = new Set(declared.filter((name) => name.startsWith('@deepseek-ai/')))
128
+ wanted.add('@deepseek-ai/cordis')
129
+ wanted.add('@deepseek-ai/schemastery')
130
+
131
+ const paths = {}
132
+ const missing = []
133
+ for (const name of [...wanted].sort()) {
134
+ const entry = typesEntry(scope, name.slice('@deepseek-ai/'.length))
135
+ if (entry === undefined) { missing.push(name); continue }
136
+ paths[name] = [entry]
137
+ }
138
+ // tsconfig.json maps the bare `cordis` specifier; keep that working under the
139
+ // installed layout too, where cordis is published scoped.
140
+ if (paths['@deepseek-ai/cordis'] !== undefined) paths['cordis'] = paths['@deepseek-ai/cordis']
141
+
142
+ if (missing.length > 0) {
143
+ fail('installed DSH is missing the packages this plugin imports: ' + missing.join(', ') + '\n (looked in ' + scope + ')')
144
+ }
145
+
146
+ // The generated config must live in the repo root: tsconfig.json's `include`
147
+ // is relative to the config that declares it, so a config placed elsewhere
148
+ // would look for src/ in the wrong directory. Every mapped path is absolute,
149
+ // so only `include`/`extends` depend on the location. Removed again below.
150
+ const configPath = join(ROOT, '.dsh-compat.tsconfig.json')
151
+ writeFileSync(configPath, JSON.stringify({ extends: './tsconfig.json', compilerOptions: { paths } }, null, 2))
152
+
153
+ // Run tsc's JS entry through the current node binary. Spawning the .bin shell
154
+ // shim instead would need a shell on Windows (npm .cmd/.bat shims cannot be
155
+ // spawned directly), and this way the exit code and output stay unambiguous.
156
+ const tsc = join(ROOT, 'node_modules', 'typescript', 'bin', 'tsc')
157
+ if (!existsSync(tsc)) { rmSync(configPath, { force: true }); fail('local typescript not found at ' + tsc + ' (run the package manager install first)') }
158
+
159
+ console.log('check-dsh-compat: type-checking src/ against installed dsh ' + dshVersion)
160
+ console.log('check-dsh-compat: scope ' + scope)
161
+ let failure
162
+ try {
163
+ execFileSync(process.execPath, [tsc, '-p', configPath, '--noEmit'], { cwd: ROOT, stdio: 'inherit' })
164
+ } catch (error) {
165
+ failure = error
166
+ } finally {
167
+ rmSync(configPath, { force: true })
168
+ }
169
+ if (failure !== undefined) {
170
+ const status = typeof failure.status === 'number' ? ' (tsc exit ' + failure.status + ')' : ''
171
+ fail('src/ does NOT type-check against installed dsh ' + dshVersion + status + ' — this is a real incompatibility with the running harness.')
172
+ }
173
+ console.log('check-dsh-compat: OK — src/ type-checks against installed dsh ' + dshVersion)
package/src/tools.ts CHANGED
@@ -194,8 +194,16 @@ function registerCreate(env: BridgeEnv): void {
194
194
  ...(model !== '' ? { model } : {}),
195
195
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
196
196
  },
197
- setup: (agentCtx: Context) => {
198
- if (caller !== undefined) {
197
+ setup: async (agentCtx: Context) => {
198
+ const requestedPreset = typeof args.agentPreset === 'string' && args.agentPreset.trim() !== '' ? args.agentPreset.trim() : undefined
199
+ if (requestedPreset !== undefined) {
200
+ // An explicit agentPreset composes THAT preset. Falling through to the
201
+ // caller-join below would silently ignore the request: the id would
202
+ // still reach the session header, while the agent kept running on the
203
+ // caller's standing composition and its tools.
204
+ const presets = env.ctx.agentPresets as unknown as { mount(agentCtx: Context, id: string): Promise<unknown> }
205
+ await presets.mount(agentCtx, requestedPreset)
206
+ } else if (caller !== undefined) {
199
207
  const presets = env.ctx.agentPresets as unknown as { composeFrom(agentCtx: Context, parentCtx: Context): string | undefined }
200
208
  presets.composeFrom(agentCtx, caller.ctx)
201
209
  }