dsh-gitbash-shell 0.31.0 → 0.32.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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * dsh-gitbash-shell/schemastery — resolve the ONE schemastery copy whose
3
+ * semantics match the executing host, instead of whichever copy Node finds
4
+ * first.
5
+ *
6
+ * WHY THIS MODULE EXISTS (issue #12). `@deepseek-ai/schemastery` is a peer, so
7
+ * it is resolved by walking UP from this plugin's own file. In a hoisted
8
+ * profile that walk can land on an OLDER copy another dependency hoisted to the
9
+ * profile root — measured on the reporter's machine: 3.18.2, which has no
10
+ * `volatile()` — while the host itself runs the copy installed beside its own
11
+ * shell packages (3.18.4). The executor's base class comes from the HOST's copy
12
+ * of `@deepseek-ai/dsh-bash-sandbox`: on dsh 0.1.7+ it declares its limits with
13
+ * `.volatile()` and reads every one of them through `.get()`
14
+ * (`packages/shell/bash-local/src/index.ts:100-107`, `assertServiceableBashConfig`).
15
+ * A subclass whose Config was built from a `volatile`-less schemastery
16
+ * therefore yields PLAIN values and the first shell call dies with
17
+ * `TypeError: config.timeoutMs.get is not a function`, taking the whole
18
+ * `ctx.shell` down with it. Issue #6 was the same crash from the other
19
+ * direction (an unconditional plain Config); v0.24.4 fixed that with a
20
+ * per-field `schema.volatile` probe, which only helps when the copy we happen
21
+ * to resolve HAS `volatile()`.
22
+ *
23
+ * The choice is therefore driven by the HOST'S BEHAVIOUR, never by our own
24
+ * resolution order. Asking the base class what it resolves `timeoutMs` to is a
25
+ * direct read of "does this host call `.get()`?" — true on 0.1.7+, false on
26
+ * <= 0.1.6 where plain values are the correct answer. Only when the host wants
27
+ * Volatile refs do we insist on a copy that provides them, and only then do we
28
+ * try the parent package's own resolution domain first. An unreadable parent
29
+ * Config keeps the historical behaviour (our own copy, per-field probe), so an
30
+ * unknown host is never made worse.
31
+ */
32
+
33
+ import { createRequire } from 'node:module'
34
+ import { pathToFileURL } from 'node:url'
35
+
36
+ /** The peer whose copy must agree with the host. */
37
+ export const SCHEMASTERY_PEER = '@deepseek-ai/schemastery'
38
+
39
+ /** The package whose resolution domain is authoritative — the executor we extend. */
40
+ export const SCHEMASTERY_PARENT = '@deepseek-ai/dsh-bash-sandbox'
41
+
42
+ const TAG = '[gitbash-shell]'
43
+
44
+ /** Node's own answer for `specifier` as seen from `base`, or `''` when it fails. */
45
+ export function resolveFrom(base, specifier) {
46
+ try {
47
+ return createRequire(base).resolve(specifier)
48
+ } catch {
49
+ return ''
50
+ }
51
+ }
52
+
53
+ /** Whether one loaded module looks like a usable schemastery. */
54
+ export function isSchemastery(z) {
55
+ return !!z && typeof z.object === 'function'
56
+ }
57
+
58
+ /** Whether one loaded schemastery can produce Volatile refs. */
59
+ export function hasVolatile(z) {
60
+ if (!isSchemastery(z)) return false
61
+ try {
62
+ return typeof z.string().volatile === 'function'
63
+ } catch {
64
+ return false
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Ask the executing host whether it reads its shell limits through `.get()`.
70
+ *
71
+ * `SandboxBashExecutor.Config({})` is the host's OWN schema, built from the
72
+ * host's own schemastery: it resolves `timeoutMs` to a `Volatile` ref exactly
73
+ * when the host declares volatile fields. Behaviour, not internals — no private
74
+ * schema shape is read.
75
+ *
76
+ * @param ParentConfig - the base class's `static Config`, if it has one.
77
+ * @returns `true`/`false` when the host answered, `undefined` when it could not.
78
+ */
79
+ export function hostWantsVolatileRefs(ParentConfig) {
80
+ if (typeof ParentConfig !== 'function') return undefined
81
+ try {
82
+ const resolved = ParentConfig({})
83
+ if (!resolved || typeof resolved !== 'object') return undefined
84
+ return typeof resolved.timeoutMs?.get === 'function'
85
+ } catch {
86
+ return undefined
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Pick one already-loaded copy. Pure, so the matrix is testable without a
92
+ * filesystem: the caller supplies the candidates and their resolution order.
93
+ *
94
+ * @param options - the host's answer, our own entry path, and the loaded copies in priority order.
95
+ * @returns the chosen copy (or undefined) and why, for the log line.
96
+ */
97
+ export function chooseCopy({ needsVolatile, ownEntry, copies }) {
98
+ const own = copies.find((copy) => copy.entry === ownEntry)
99
+ if (copies.length === 0) return { chosen: undefined, reason: 'unresolved' }
100
+ if (needsVolatile === false) {
101
+ // <= 0.1.6 reads plain values: the historical answer is the correct one.
102
+ return { chosen: own ?? copies[0], reason: 'host-plain' }
103
+ }
104
+ if (needsVolatile === true) {
105
+ // Our own copy is the historical answer: keep it whenever it can serve, and
106
+ // redirect ONLY when it cannot (that redirection is the whole point of #12).
107
+ if (own !== undefined && hasVolatile(own.z)) return { chosen: own, reason: 'host-volatile' }
108
+ const capable = copies.find((copy) => hasVolatile(copy.z))
109
+ return capable
110
+ ? { chosen: capable, reason: 'host-volatile-redirected' }
111
+ : { chosen: own ?? copies[0], reason: 'no-volatile-anywhere' }
112
+ }
113
+ // The host could not answer: keep the pre-#12 behaviour (our own copy) rather
114
+ // than guessing a semantics change on its behalf.
115
+ return { chosen: own ?? copies[0], reason: 'host-unknown' }
116
+ }
117
+
118
+ /** Load one module by absolute entry path. */
119
+ async function loadAt(entry) {
120
+ const mod = await import(pathToFileURL(entry).href)
121
+ return mod?.default ?? mod
122
+ }
123
+
124
+ /** One process-wide answer: the module graph does not change under us. */
125
+ let cached = null
126
+
127
+ /**
128
+ * Resolve, load, and cache the schemastery copy that matches the executing host.
129
+ *
130
+ * @param options - `parentConfig` is the executor base class's `static Config`.
131
+ * @returns the chosen module (or undefined), where it came from, the host's answer, and the reason.
132
+ */
133
+ export async function loadSchemastery({ parentConfig } = {}) {
134
+ if (cached !== null) return cached
135
+ const needsVolatile = hostWantsVolatileRefs(parentConfig)
136
+ const ownEntry = resolveFrom(import.meta.url, SCHEMASTERY_PEER)
137
+
138
+ const wanted = []
139
+ const parentEntry = resolveFrom(import.meta.url, SCHEMASTERY_PARENT)
140
+ if (parentEntry !== '') {
141
+ const beside = resolveFrom(parentEntry, SCHEMASTERY_PEER)
142
+ if (beside !== '' && beside !== ownEntry) wanted.push({ where: 'the shell packages', entry: beside })
143
+ }
144
+ if (ownEntry !== '') wanted.push({ where: 'this plugin', entry: ownEntry })
145
+
146
+ const copies = []
147
+ for (const candidate of wanted) {
148
+ try {
149
+ const z = await loadAt(candidate.entry)
150
+ if (isSchemastery(z)) copies.push({ where: candidate.where, entry: candidate.entry, z })
151
+ } catch { /* try the next domain */ }
152
+ }
153
+
154
+ const { chosen, reason } = chooseCopy({ needsVolatile, ownEntry, copies })
155
+ const result = { z: chosen?.z, source: chosen?.where ?? '', entry: chosen?.entry ?? '', needsVolatile, reason }
156
+ cached = result
157
+
158
+ if (result.z === undefined) {
159
+ console.warn(`${TAG} ${SCHEMASTERY_PEER} is not resolvable from this plugin or from ${SCHEMASTERY_PARENT}; the row Config surface is absent`)
160
+ } else if (reason === 'no-volatile-anywhere') {
161
+ // The one case the plugin cannot paper over: the host calls `.get()` on
162
+ // plain values, so every shell call would fail. Say so where the user will
163
+ // see it, instead of leaving a mystery TypeError.
164
+ console.warn(
165
+ `${TAG} this host reads its shell limits through .get() but NO resolvable ${SCHEMASTERY_PEER} provides volatile()`
166
+ + ` (tried: ${wanted.map((w) => `${w.where} -> ${w.entry}`).join('; ') || 'nothing resolved'}).`
167
+ + ' Shell calls will fail with "config.timeoutMs.get is not a function"; install a dsh whose schemastery matches its own shell packages.',
168
+ )
169
+ } else if (chosen.where === 'the shell packages') {
170
+ console.log(`${TAG} ${SCHEMASTERY_PEER} taken from the shell packages instead of this plugin's own resolution (${reason}): ${chosen.entry}`)
171
+ }
172
+ return result
173
+ }
174
+
175
+ /** Test seam: forget the cached answer. */
176
+ export function resetSchemasteryCache() {
177
+ cached = null
178
+ }
package/src/shell.js CHANGED
@@ -43,7 +43,7 @@ import { DEFAULT_GIT_BASH, bashResolutionReport, effectiveConfiguredBashPath, re
43
43
 
44
44
  /** Re-exported for callers that historically read it here (the value now lives in bash-path.js). */
45
45
  export { DEFAULT_GIT_BASH }
46
- import z from '@deepseek-ai/schemastery'
46
+ import { loadSchemastery } from './schemastery.js'
47
47
 
48
48
 
49
49
  /** Log prefix, matching src/index.js. */
@@ -77,7 +77,10 @@ const SETTINGS_NAMESPACE = 'gitbash-shell'
77
77
  * ones. Published schemastery 3.18.2 has no `volatile()`, which is exactly why
78
78
  * the probe (not an unconditional call) is required.
79
79
  */
80
- function live(schema) {
80
+ function live(schema, needsVolatile) {
81
+ // A <=0.1.6 host reads plain values: never hand it a ref it will not `.get()`
82
+ // (issue #12 — the host's own answer wins over what our copy happens to offer).
83
+ if (needsVolatile === false) return schema
81
84
  return typeof schema.volatile === 'function' ? schema.volatile() : schema
82
85
  }
83
86
 
@@ -86,16 +89,19 @@ function live(schema) {
86
89
  * the smoke test can drive BOTH eras with a recording stand-in instead of
87
90
  * depending on which schemastery happens to be installed here.
88
91
  * @param z schemastery module (real one, or the test's recorder).
92
+ * @param needsVolatile - the host's OWN answer from src/schemastery.js: `true`
93
+ * forces Volatile refs, `false` forces plain values, `undefined` keeps the
94
+ * per-field probe (the historical behaviour, kept for an unreadable host).
89
95
  * @returns the object schema for the `gitbash-executor` row.
90
96
  */
91
- export function gitBashShellConfig(z) {
97
+ export function gitBashShellConfig(z, needsVolatile) {
92
98
  return z.object({
93
- cwd: live(z.string()),
94
- timeoutMs: live(z.number().default(120000)),
95
- maxTimeoutMs: live(z.number().default(600000)),
96
- maxOutputBytes: live(z.number().default(64000)),
97
- maxSpillBytes: live(z.number().default(64 * 1024 * 1024)),
98
- graceMs: live(z.number().default(3000)),
99
+ cwd: live(z.string(), needsVolatile),
100
+ timeoutMs: live(z.number().default(120000), needsVolatile),
101
+ maxTimeoutMs: live(z.number().default(600000), needsVolatile),
102
+ maxOutputBytes: live(z.number().default(64000), needsVolatile),
103
+ maxSpillBytes: live(z.number().default(64 * 1024 * 1024), needsVolatile),
104
+ graceMs: live(z.number().default(3000), needsVolatile),
99
105
  // Our own knob: the base class neither declares nor `.get()`s it, and
100
106
  // `get bashPath()` below reads it as a plain value, so it stays plain.
101
107
  //
@@ -107,8 +113,21 @@ export function gitBashShellConfig(z) {
107
113
  })
108
114
  }
109
115
 
116
+ /**
117
+ * The schemastery copy that matches the executing host (see src/schemastery.js).
118
+ * Resolving our own way can land on an OLDER copy another dependency hoisted to
119
+ * the profile root — one without `volatile()` — and the executor then declares
120
+ * plain limits for a base class that calls `.get()` on them: every shell call
121
+ * dies with `config.timeoutMs.get is not a function` (issue #12).
122
+ *
123
+ * Deliberately ONE line, and the ONLY top-level await in this module: the smoke
124
+ * harness evaluates this body inside `new Function`, where an await cannot
125
+ * appear, so it swaps exactly this expression for a stub.
126
+ */
127
+ const schema = await loadSchemastery({ parentConfig: SandboxBashExecutor.Config })
128
+
110
129
  /** Resolved configuration: the local executor's knobs, plus the Git Bash path. */
111
- export const Config = gitBashShellConfig(z)
130
+ export const Config = schema.z ? gitBashShellConfig(schema.z, schema.needsVolatile) : undefined
112
131
 
113
132
  /** Git Bash executor — mirrors the shipped bash/pwsh sandbox executors. */
114
133
  export class GitBashSandboxExecutor extends SandboxBashExecutor {
@@ -1,282 +0,0 @@
1
- # ERA (persona-split): dsh >= 0.1.3-alpha.2 text — the persona row uses the
2
- # `prefix:`/`suffix:` keys (40792330c0 split dsh-persona single text key).
3
- # The pre-split texts live in the sibling files named in the banner below; the
4
- # plugin probes the roster AND the persona form at materialization and writes
5
- # the picked text as the preset own agent.cordis.yml.
6
- # ERA: dsh >= 0.1.2 text (built-in preset id `ptc`, presented with mode: ptc).
7
- # The dsh <= 0.1.1 era text (built-in id `code`) lives in agent.cordis.yml; the
8
- # plugin probes the roster at materialization and writes the picked text as the
9
- # preset's own agent.cordis.yml.
10
- # The `ptc` agent preset: the standard coding agent, presented as PTC mode.
11
- #
12
- # Most of `standard` is here unchanged. The deliberate exception is the
13
- # general-purpose `workflow` tool: PTC mode uses `run_code` as its model-authored
14
- # composition surface. The `tool-presentation` row turns the remaining registry
15
- # into a generated SDK, so a sequence that would be five round trips becomes one.
16
- #
17
- # The registry itself stays on the host plane — the agent loop's scheduler and
18
- # the API proxy's presenters are its consumers — so what this preset owns is
19
- # the PRESENTATION of that registry for this agent alone. Native sessions run
20
- # beside this one in the same process, each seeing its own catalog.
21
- #
22
- # This file is an AGENT-PLANE composition. It is mounted under one agent's
23
- # scope context, so every tool and prompt section it registers belongs to that
24
- # session alone. The host composition (`base.cordis.yml` + `web.cordis.yml`)
25
- # keeps everything a preset must not own: the registries themselves, the
26
- # sandbox and approval stack, persistence, and the model route.
27
- #
28
- # A service row here MUST sit inside a group carrying an `isolate` realm.
29
- # Without one it publishes into the root realm, where it is process-global
30
- # rather than per-session and the second session mounting this preset collides
31
- # with the first; `dsh-agent-presets` rejects that at mount. `true` means an
32
- # entry-local realm — one private instance per mounted session, which is the
33
- # default this deployment wants. A shared label would instead pool one instance
34
- # across every session naming it.
35
-
36
- # ── identity ────────────────────────────────────────────────────────────────
37
-
38
- # The preset's own persona, shadowing the deployment default for this agent.
39
- # `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
40
- - id: persona
41
- name: '@deepseek-ai/dsh-persona'
42
- config:
43
- suffix: Your working directory is {{cwd}}.
44
- prefix: >-
45
- You are a coding agent powered by the {{model}} model.
46
-
47
- - id: agent-instructions
48
- name: '@deepseek-ai/dsh-agent-instructions'
49
- config:
50
- maxBytes: 65536
51
-
52
- # ── shell ───────────────────────────────────────────────────────────────────
53
-
54
- # `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
55
- # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
56
- # the criterion for host-plane ownership — injection resolves before any session
57
- # exists, so there is no agent to key by. Behind a preset realm those variables
58
- # never reached the model's shell at all. Both shell tools consume the host
59
- # registry from here; the host executor is the Git Bash provider (`dsh-gitbash-shell`)
60
- # host-plane too.
61
- - id: tool-bash
62
- name: '@deepseek-ai/dsh-tool-bash'
63
- # Git Bash on Windows: the host executor is dsh-gitbash-shell (see $DSH_HOME/cordis.patch.yml).
64
- disabled: false
65
-
66
- - id: tool-pwsh
67
- name: '@deepseek-ai/dsh-tool-pwsh'
68
- disabled: true
69
-
70
- # ── filesystem ──────────────────────────────────────────────────────────────
71
-
72
- # Both register into the host `tools` registry and provide nothing, so
73
- # they need no realm. The `fs` service and its policy stay in the host.
74
- - id: tool-fs
75
- name: '@deepseek-ai/dsh-tool-fs'
76
-
77
- - id: tool-fs-search
78
- name: '@deepseek-ai/dsh-tool-fs-search'
79
- config:
80
- sampleOverCapGlobResults: false
81
-
82
- # ── background jobs ────────────────────────────────────────────────────────
83
-
84
- # Only the model-facing controls. The task REGISTRY stays on the host plane:
85
- # its producers sit outside any realm this file could put it in — `tool-bash`
86
- # above resolves it with `ctx.get`, and an entry-local realm here is invisible
87
- # to every sibling row, so `run_in_background` would answer "background jobs
88
- # unavailable" while these controls sat in the catalog. The registry is keyed by
89
- # owning agent anyway, so one host instance serves every session. What a preset
90
- # chooses is whether its agent can collect and stop background work at all.
91
- - id: tool-jobs
92
- name: '@deepseek-ai/dsh-tool-jobs'
93
-
94
- # ── skills ──────────────────────────────────────────────────────────────────
95
-
96
- # The skill REGISTRY lives in the host composition and is layered per scope:
97
- # these rows register into THIS preset's layer of it, so they need no realm.
98
- # `skill-filesystem` contributes local-root discovery for agents on this preset, and
99
- # `tool-skill` gives them the catalog and loader; the merged catalog also
100
- # carries whatever the deployment registered globally (repository plugins).
101
- - id: skill-filesystem
102
- name: '@deepseek-ai/dsh-skill-filesystem'
103
-
104
- - id: tool-skill
105
- name: '@deepseek-ai/dsh-tool-skill'
106
-
107
- # ── goals ───────────────────────────────────────────────────────────────────
108
-
109
- # The goal service and session driver stay on the host plane, where the Gateway
110
- # can resolve them. The human command and model-facing tool register into this
111
- # preset's scoped layers.
112
- - id: command-goal
113
- name: '@deepseek-ai/dsh-command-goal'
114
-
115
- - id: tool-goal
116
- name: '@deepseek-ai/dsh-tool-goal'
117
-
118
- # ── plan mode ───────────────────────────────────────────────────────────────
119
-
120
- # Plan state is per-agent by nature, so an entry-local realm is not a
121
- # workaround here — it is the correct lifetime.
122
- - id: planning
123
- name: cordis:group
124
- group: true
125
- isolate:
126
- planMode: true
127
- config:
128
- - id: plan-mode
129
- name: '@deepseek-ai/dsh-plan-mode'
130
- config:
131
- section: |
132
- You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
133
-
134
- Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
135
-
136
- The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
137
-
138
- Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
139
-
140
- Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
141
-
142
- When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
143
-
144
- # ── compaction ──────────────────────────────────────────────────────────────
145
-
146
- # `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
147
- # share this realm rather than sit outside it.
148
- #
149
- # `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
150
- # plane, and the rows here resolve that one instance. It takes no configuration,
151
- # keys every fold by Session, and owns the context-meter projection units the
152
- # browser reads for every session — behind a realm those units would come and go
153
- # with whichever presets happen to be mounted. What a preset chooses is whether
154
- # its agent compacts at all, which is `compaction-basic` below.
155
- - id: compaction
156
- name: cordis:group
157
- group: true
158
- isolate:
159
- compaction: true
160
- toolResultPruner: true
161
- config:
162
- - id: compaction-basic
163
- name: '@deepseek-ai/dsh-compaction-basic'
164
-
165
- - id: command-compact
166
- name: '@deepseek-ai/dsh-command-compact'
167
-
168
- - id: tool-result-pruner
169
- name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
170
- config:
171
- thresholdChars: 8192
172
- headChars: 4096
173
- tailChars: 1024
174
-
175
- # ── delegation and workflows ────────────────────────────────────────────────
176
-
177
- # The `subagents` registry and its spawn/fork backends live in the HOST
178
- # composition: the registry is a process singleton whose cross-session queries
179
- # the api-proxy serves to the browser, and a provider name may only be
180
- # registered once. This preset contributes the delegation TOOLS, which resolve
181
- # that host registry.
182
- #
183
- # `workflows` is different — nothing outside an agent reads it — so every row
184
- # that reaches it shares one entry-local realm here, and a consumer left
185
- # outside would resolve a host registry this preset does not populate.
186
- - id: delegation
187
- name: cordis:group
188
- group: true
189
- isolate:
190
- workflowEngine: true
191
- config:
192
- - id: tool-subagent-control
193
- name: '@deepseek-ai/dsh-tool-subagent-control'
194
-
195
- - id: tool-subagent-list-agents
196
- name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
197
-
198
- - id: tool-subagent
199
- name: '@deepseek-ai/dsh-tool-subagent'
200
- config:
201
- provider: spawn
202
- toolName: subagent
203
- modelSelectionSettings: true
204
- backgroundMode: continuable
205
-
206
- # Fork omits model selection so provider/model stay equal to the parent and
207
- # the inherited history remains eligible for KV Cache reuse. This preset
208
- # keeps fork continuable; parent and child inherit the same messaging tool,
209
- # while the parent id and return guidance follow the inherited history.
210
- - id: tool-subagent-fork
211
- name: '@deepseek-ai/dsh-tool-subagent'
212
- config:
213
- provider: fork
214
- toolName: subagent_fork
215
- backgroundMode: continuable
216
-
217
- # Production dsh does not install these optional providers. Install the
218
- # matching Bundle in this Profile and restart the Host, then copy this
219
- # preset and remove `disabled` from the matching tool row. Host availability
220
- # alone grants no tool.
221
- - id: tool-subagent-codex
222
- name: '@deepseek-ai/dsh-tool-subagent'
223
- disabled: true
224
- config:
225
- provider: codex
226
- toolName: subagent_codex
227
- backgroundMode: one-shot
228
- maxDepth: provider-managed
229
-
230
- - id: tool-subagent-claude-code
231
- name: '@deepseek-ai/dsh-tool-subagent'
232
- disabled: true
233
- config:
234
- provider: claude-code
235
- toolName: subagent_claude_code
236
- backgroundMode: one-shot
237
- maxDepth: provider-managed
238
-
239
- - id: workflow-worker-thread
240
- name: '@deepseek-ai/dsh-workflow-worker-thread'
241
- config:
242
- provider: spawn
243
-
244
- - id: tool-workflow
245
- name: '@deepseek-ai/dsh-tool-workflow'
246
- # Keep the engine above for `ralph`, but do not publish a second
247
- # model-authored orchestration surface beside `run_code` in PTC mode.
248
- disabled: true
249
-
250
- - id: tool-ralph
251
- name: '@deepseek-ai/dsh-tool-ralph'
252
- config:
253
- subagentProvider: spawn
254
- maxRounds: 64
255
-
256
- # ── remaining model-facing rows ─────────────────────────────────────────────
257
-
258
- - id: tool-ask-user
259
- name: '@deepseek-ai/dsh-tool-ask-user'
260
-
261
- - id: tool-todo
262
- name: '@deepseek-ai/dsh-tool-todo'
263
- config:
264
- allowParallelInProgress: true
265
-
266
- # The `web` service and its search provider stay in the host composition; only
267
- # the model-facing tool is per-session.
268
- - id: tool-web
269
- name: '@deepseek-ai/dsh-tool-web'
270
- config:
271
- fetch: true
272
- searchTimeoutMs: 60000
273
-
274
- # ── presentation ────────────────────────────────────────────────────────────
275
-
276
- # PTC mode for this agent alone. The row waits for the host's `codeRuntime`
277
- # rather than assuming it: a deployment that composes no TypeScript runtime
278
- # fails this preset at mount, naming this id, instead of at the first request.
279
- - id: tool-presentation
280
- name: '@deepseek-ai/dsh-agent-tool-presentation'
281
- config:
282
- mode: ptc