dsh-switchman 1.11.2 → 1.11.3

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
@@ -154,7 +154,7 @@ All keys live in Settings → dsh-switchman; defaults from `host/config.js`:
154
154
 
155
155
  ## How it works
156
156
 
157
- - The host half (`index.js` + `host/`) injects the byte-stable system-prompt sections (language / lanes / watermark protocol / teams) plus the live watermark line through the tail runtime-context snapshot (a channel that appends instead of rewriting the prompt head, so the provider prompt cache stays intact), the read-budget plus enforce double gate, and four slash commands (the ctx trio + `/vision`). A saved setting takes effect at the very next prompt assembly — no restart.
157
+ - The host half (`index.js` + `host/`) injects the byte-stable system-prompt sections (language / lanes / watermark protocol / teams / PTC guard) plus the live watermark line through the tail runtime-context snapshot (a channel that appends instead of rewriting the prompt head, so the provider prompt cache stays intact), the read-budget plus enforce double gate, and four slash commands (the ctx trio + `/vision`). A saved setting takes effect at the very next prompt assembly — no restart. In PTC sessions (programmatic tool calls) the PTC guard renders as the final section and restates that every named tool must be invoked inside a `run_code` program (`await tools.name(args)`), making `unknown tool` rejections much less likely when doctrines or skill catalogs name tools directly (a prompt-level mitigation, not a hard guarantee).
158
158
  - The client half (`client.js`) renders the settings page, the ⚡ badge and ◇ model marker in the session header, and the live handover indicator; config snapshots are read and written through the Host route family.
159
159
  - The "Switchman Dispatch Center" entry in the home sidebar opens that same settings page as a center panel in one click; the original settings entry stays.
160
160
  - `cordis.patch.yml` keeps the factory preset plugin list fully intact and only extends the persona suffix; the Agent Teams tools themselves come from the factory bundle and switch on automatically with teams mode.
@@ -162,7 +162,7 @@ All keys live in Settings → dsh-switchman; defaults from `host/config.js`:
162
162
  ## Maintenance notes
163
163
 
164
164
  - After a DSH upgrade, if the factory preset plugin list changed, re-sync `cordis.patch.yml` from the new `presets/*.patch.yml` (keeping the doctrine suffix and the compaction arbitration — `auto: false` on the `compaction-basic` rows), then reinstall.
165
- - Protocol lines (`[SWITCHMAN:LANG|POOLS|WATERMARK|TEAMS]`) are deliberately English and byte-stable — don't localize them.
165
+ - Protocol lines (`[SWITCHMAN:LANG|POOLS|WATERMARK|TEAMS|PTC]`) are deliberately English and byte-stable — don't localize them.
166
166
  - `npm pack --dry-run` should keep the audited 60-file / ~380 kB tarball shape (~1.26 MB unpacked). `docs/` stays out of the package (it is not in `package.json files`), while the 11 `README.*` translations are always shipped by npm.
167
167
  - **Zero runtime dependencies — a hard invariant.** Host-plane packages (`@deepseek-ai/schemastery`, cordis, …) are declared as `peerDependencies` only, and the running DSH satisfies them from its own copy. An ordinary `dependencies` entry is physically installed into the profile workspace (`<profile>/node_modules/…`), where it shadows the host's copy for every other profile-resident plugin — this is exactly what crashed the dshmarket plugin market while 1.7.0 carried schemastery as a dependency (two schemastery instances in one process; the market's schemas no longer matched the host's). `scripts/validate.mjs` fails if a `dependencies` entry ever reappears, so installing, updating, or removing dsh-switchman can never touch what dshmarket or any other community bundle resolves at runtime.
168
168
 
@@ -0,0 +1,114 @@
1
+ /** dsh-switchman PTC direct-call guard layer.
2
+ *
3
+ * Symptom (observed 2026-11-12, session-level): in PTC sessions the model
4
+ * calls a named tool DIRECTLY — e.g. `spawn_teammate` or `skill` after the
5
+ * teams doctrine or the skill catalog names it — and the harness rejects the
6
+ * whole call with `unknown tool "x": only \`run_code\` is callable directly`.
7
+ * The PTC executor's own `PTC_ONLY` section (order 800) states the rule once
8
+ * near the top, but every later doctrine that names tools directly erodes it.
9
+ *
10
+ * Second symptom (same day, session 22ced69f): the model sets `sandbox_permissions`
11
+ * on a call while the session already runs `danger-full-access`; the escalation
12
+ * request is rejected outright with `sandbox escalation to "workspace-write" is
13
+ * not strictly wider than this call's current "danger-full-access" mode` — an
14
+ * error with no correction hint. The clause below also states the never-set
15
+ * default and the retry-without-the-field fix.
16
+ *
17
+ * Fix: one extra dynamic systemPrompt section (order 10510 — AFTER every
18
+ * other dsh-switchman section: teams 10250, lang 10300, dispatch 10400,
19
+ * context-trim 10450, context-watch 10500) that re-states the transport rule
20
+ * as the LAST word whenever the viewing scope actually resolves to the PTC
21
+ * executor. Native scopes render an empty string (status quo).
22
+ *
23
+ * Detection mirrors the ToolsService's own per-scope resolution: the section
24
+ * text closure receives the assemble context, whose `scope` is the viewing
25
+ * Agent/preset scope key; `ctx.tools.selectExecutor(scope)` (TS-private but
26
+ * present at runtime — verified in the packaged app, `selectExecutor` survives
27
+ * bundling) resolves the nearest scoped executor or the deployment default.
28
+ * The executor is then discriminated BEHAVIORALLY via its public `describe()`:
29
+ * the PTC executor declares `knownNames: ["run_code"]` exactly; the native
30
+ * executor returns no `knownNames` at all. No class-name sniffing, no config
31
+ * reads, no false positives for other executor implementations (they simply
32
+ * get no guard).
33
+ *
34
+ * Fail-open everywhere: a missing/renamed private API, a throwing describe(),
35
+ * or an unavailable executor selection all render "" — the session keeps its
36
+ * current behavior and only loses the mitigation. Zero new dependencies.
37
+ *
38
+ * The clause is English, byte-stable protocol text (same convention as the
39
+ * lang layer) with a `[SWITCHMAN:PTC]` anchor, `interpolate: false` because
40
+ * the text contains braces.
41
+ */
42
+
43
+ /** Reserved transport-tool name the PTC executor declares as its only direct call. */
44
+ const RUN_CODE_NAME = "run_code";
45
+
46
+ /** Final-say section position: after context-watch (10500), the last existing section. */
47
+ const PTC_GUARD_ORDER = 10510;
48
+
49
+ /** True when an executor description declares exactly the reserved run_code transport. */
50
+ function isPtcDescription(description) {
51
+ if (description === null || typeof description !== "object") return false;
52
+ const known = description.knownNames;
53
+ return Array.isArray(known) && known.length === 1 && known[0] === RUN_CODE_NAME;
54
+ }
55
+
56
+ /** Selected executor for one assemble scope, or null when the private probe
57
+ * is unusable (missing service, missing/renamed method, unavailable selection). */
58
+ function selectedExecutor(ctx, scope) {
59
+ const tools = ctx?.tools;
60
+ if (tools === null || typeof tools !== "object") return null;
61
+ if (typeof tools.selectExecutor !== "function") return null;
62
+ try {
63
+ const executor = tools.selectExecutor(scope);
64
+ return executor === null || typeof executor !== "object" ? null : executor;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /** True when the scope's selected executor is the PTC executor. `describe([])`
71
+ * is safe for every known executor: the PTC implementation lazily creates its
72
+ * transport there (the ToolsService calls it on every model call anyway) and
73
+ * the native one maps over the empty list. */
74
+ function isPtcScope(ctx, scope) {
75
+ const executor = selectedExecutor(ctx, scope);
76
+ if (executor === null || typeof executor.describe !== "function") return false;
77
+ try {
78
+ return isPtcDescription(executor.describe([]));
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ /** Byte-stable protocol clause. The SDK call shape `await tools.name(args)` is
85
+ * quoted verbatim from the run_code tool's own model-facing description. */
86
+ function renderPtcGuard() {
87
+ return [
88
+ `[SWITCHMAN:PTC] This session runs in PTC (programmatic tool call) mode: \`${RUN_CODE_NAME}\` is the ONLY tool callable directly.`,
89
+ `- Every other tool MUST be invoked as an awaited SDK function inside a ${RUN_CODE_NAME} program: \`await tools.name(args)\` — e.g. \`await tools.skill({ name: "git-commit-message" })\` or \`await tools.spawn_teammate({ ... })\`.`,
90
+ `- A direct call fails with: unknown tool "x": only \`${RUN_CODE_NAME}\` is callable directly — call \`x\` from inside a \`${RUN_CODE_NAME}\` program instead. Never retry the direct form; wrap the call.`,
91
+ `- Wherever this prompt says "call the X tool" (skill catalog, doctrines, per-turn reminders), read it as "call X inside a ${RUN_CODE_NAME} program". Batch independent calls as parallel awaits inside ONE program.`,
92
+ `- Never set \`sandbox_permissions\` on any call, direct or SDK: it is a privilege-escalation request and fails with "not strictly wider than this call's current ... mode" whenever the session already runs at least that access level — retry without the field.`,
93
+ ].join("\n");
94
+ }
95
+
96
+ /** Mount the PTC guard section. Section registration itself relies on the
97
+ * injected systemPrompt service (same as every other layer); the text closure
98
+ * is fully defensive so an assembly in an unexpected shape renders "". */
99
+ function applyPtcGuard(ctx) {
100
+ ctx.systemPrompt.section({
101
+ name: "switchman:ptc-guard",
102
+ order: PTC_GUARD_ORDER,
103
+ interpolate: false,
104
+ text: (context) => {
105
+ try {
106
+ return isPtcScope(ctx, context?.scope) ? renderPtcGuard() : "";
107
+ } catch {
108
+ return "";
109
+ }
110
+ },
111
+ });
112
+ }
113
+
114
+ export { applyPtcGuard, isPtcDescription, isPtcScope, renderPtcGuard, selectedExecutor };
package/host/teams.js CHANGED
@@ -86,6 +86,7 @@ const TEAMS_SECTION_TEXT = [
86
86
  "- teams 不可用即降级,不重试:工具目录没有 spawn_teammate/list_agents(bundle 未生效或本会话不支持),或 spawn_teammate 报 provisioning failed / 创建失败时,直接改用 subagent/subagent_fork 派发并继续工作,不要反复重试 teams 通道。你自身是 teammate(非 Lead)时维持原规则:不 spawn_teammate,需要子委派时用 subagent。workflow 仅在用户明确要求工作流或脚本化大规模编排时使用,默认不主动使用。",
87
87
  "- 写作域隔离:共享任务上写明互不重叠的 write_scopes;用 team_task_* 维护任务板与依赖顺序;Lead 汇总最终 diff、复核关键改动并运行测试;用 wait_agent 等到必需的 teammate 完成,绝不在结果未回前下最终结论。",
88
88
  "- 子智能体消息通道:subagent/workflow 返回的子会话 id 只能经 child_message 桥接送达——send_message 只解析 teammate 名字,对子会话 id 必报 active teammate「…」not found;该报错不代表子会话死亡,先改用 child_message,不要据此重派或判定死亡。",
89
+ "- run_code 并行隔离:spawn_teammate / send_message 这类会改变团队环境(成员目录、工具表)的调用不要与文件/搜索工具放进同一 run_code 程序里 Promise.all 并行——并发窗口内其它工具绑定可能误报 unknown tool「…」(UNKNOWN_TOOL);让这类调用单独占一步执行。某工具此前可用却突然报 UNKNOWN_TOOL 时,下一步原样重试即可,不要换工具名或放弃任务。",
89
90
  ].join("\n");
90
91
 
91
92
  // ---- shared volatile-read helpers (mirrors dispatch.js) --------------------
package/index.js CHANGED
@@ -51,6 +51,13 @@
51
51
  * defensive ctx.get reads (NOT declared in inject): a deployment without the
52
52
  * llm service leaves this layer a warn-once no-op — a documented runtime
53
53
  * precondition, like sessionQuery/pluginManager above.
54
+ * The PTC direct-call guard (host/ptc-guard.js) is a final-say prompt section
55
+ * (order 10510) rendered only when the viewing scope resolves to the PTC tool
56
+ * executor — detected per assembly via ctx.tools.selectExecutor(context.scope)
57
+ * plus the executor's describe() knownNames — restating that every named tool
58
+ * must be called inside a run_code program (`await tools.name(args)`), so
59
+ * doctrines and skill catalogs that name tools directly can no longer trigger
60
+ * `unknown tool` rejections in PTC sessions.
54
61
  */
55
62
 
56
63
  import { readdirSync, readFileSync } from "node:fs";
@@ -70,6 +77,8 @@ import { applyDispatch } from "./host/dispatch.js";
70
77
  // Agent Teams layer (teams-mode doctrine section + bundle/whitelist
71
78
  // orchestration; the clause text extracted from the cordis.patch.yml suffix).
72
79
  import { applyTeams } from "./host/teams.js";
80
+ // PTC direct-call guard (final-say transport rule for PTC-executor scopes).
81
+ import { applyPtcGuard } from "./host/ptc-guard.js";
73
82
  // Subagent model-visibility layer (post-execute `model:` annotation).
74
83
  import { applySubagentModel } from "./host/subagent-model.js";
75
84
 
@@ -197,7 +206,8 @@ function loadSkillCandidates(root, warn) {
197
206
  /** Register the bundled dsh-switchman skill provider on `ctx.skills` and
198
207
  * mount the language (lang), dispatch (dispatch), teams (teams), watermark
199
208
  * (context-watch), vision (vision), intent routing (intent), subagent model
200
- * visibility (subagent-model), and client-bridge (routes) layers. */
209
+ * visibility (subagent-model), client-bridge (routes), and the PTC
210
+ * direct-call guard (ptc-guard) layers. */
201
211
  function apply(ctx, config) {
202
212
  const root = fileURLToPath(new URL("./skills/", import.meta.url));
203
213
  const warn = (message) => ctx.logger.warn(`dsh-switchman: ${message}`);
@@ -214,6 +224,7 @@ function apply(ctx, config) {
214
224
  applyLanguage(ctx, config);
215
225
  applyDispatch(ctx, config);
216
226
  applyTeams(ctx, config);
227
+ applyPtcGuard(ctx);
217
228
  applyContextWatch(ctx, config);
218
229
  applyContextTrim(ctx, config);
219
230
  applyVision(ctx, config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-switchman",
3
- "version": "1.11.2",
3
+ "version": "1.11.3",
4
4
  "description": "Autonomous Agent Teams doctrine for the four shipped DeepSeek Harness presets, plus language preferences, dispatch pools with model ranking, and context watermark control.",
5
5
  "license": "MIT",
6
6
  "keywords": [