@alwith-ai/dsh-agent 0.2.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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/README.zh.md +50 -0
- package/THIRD_PARTY_NOTICES.md +32 -0
- package/THIRD_PARTY_NOTICES.zh.md +30 -0
- package/package.json +94 -0
- package/skills/cordis-plugin-development/SKILL.md +420 -0
- package/src/bridge.ts +903 -0
- package/src/codec.ts +55 -0
- package/src/compose.ts +60 -0
- package/src/main.ts +107 -0
- package/src/oauth.ts +138 -0
- package/src/plugins-cli.ts +200 -0
- package/src/plugins.ts +747 -0
- package/src/sessions-cli.ts +78 -0
- package/src/vendor/anchored-tool-bootstrap.d.mts +19 -0
- package/src/vendor/anchored-tool-bootstrap.mjs +495 -0
package/src/plugins.ts
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manifest: the sidecar's composition expressed as data.
|
|
3
|
+
*
|
|
4
|
+
* dsh's own deployments surface every mounted plugin as a configurable row
|
|
5
|
+
* (the web UI's Plugins page); this sidecar keeps composition deterministic —
|
|
6
|
+
* the roster per preset is fixed here, in code — but exposes the same two
|
|
7
|
+
* degrees of freedom dsh gives its users: per-plugin enable/disable and
|
|
8
|
+
* per-plugin config, both read from a hand-editable overrides file.
|
|
9
|
+
*
|
|
10
|
+
* Core rows (session, llm, sandbox, approvals, …) are protected: disabling
|
|
11
|
+
* one would not produce "dsh minus a feature" but a broken runtime, so the
|
|
12
|
+
* attempt fails loud instead. Tool-surface rows are free to toggle; rows with
|
|
13
|
+
* hard capability dependencies declare `requires` and the resolver rejects a
|
|
14
|
+
* combination that would leave a mounted plugin waiting forever on a missing
|
|
15
|
+
* service (cordis would otherwise hang the mount, not error).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
22
|
+
import LlmRuntime from "@deepseek-ai/dsh-llm";
|
|
23
|
+
import SessionStore from "@deepseek-ai/dsh-session";
|
|
24
|
+
import SessionProjectionRegistry from "@deepseek-ai/dsh-session-projection";
|
|
25
|
+
import SystemPrompt from "@deepseek-ai/dsh-system-prompt";
|
|
26
|
+
import ToolRuntime from "@deepseek-ai/dsh-tools";
|
|
27
|
+
import AgentRegistry from "@deepseek-ai/dsh-agent";
|
|
28
|
+
import AgentLoop from "@deepseek-ai/dsh-agent-loop";
|
|
29
|
+
import JsonlSessionPersistence from "@deepseek-ai/dsh-session-persistence-jsonl";
|
|
30
|
+
import LocalSubprocessRuntime from "@deepseek-ai/dsh-subprocess-local";
|
|
31
|
+
import LocalSandboxProvider from "@deepseek-ai/dsh-sandbox-local";
|
|
32
|
+
import SandboxPolicyService from "@deepseek-ai/dsh-sandbox-policy";
|
|
33
|
+
import SandboxBashExecutor from "@deepseek-ai/dsh-bash-sandbox";
|
|
34
|
+
import SandboxedFileSystem from "@deepseek-ai/dsh-fs-sandbox";
|
|
35
|
+
import LocalJobRegistry from "@deepseek-ai/dsh-jobs-local";
|
|
36
|
+
import PermissionPresetService from "@deepseek-ai/dsh-permission-presets";
|
|
37
|
+
import ApprovalService from "@deepseek-ai/dsh-user-approval";
|
|
38
|
+
// Namespace imports rather than default: a module-plugin default export drops
|
|
39
|
+
// `inject` (see dsh postmortem 0001). Service classes above carry inject as a
|
|
40
|
+
// static and are safe as defaults.
|
|
41
|
+
import * as LlmDeepseek from "@deepseek-ai/dsh-llm-deepseek";
|
|
42
|
+
import * as LlmPiAi from "@deepseek-ai/dsh-llm-pi-ai";
|
|
43
|
+
import LocalCredentialProvider from "@deepseek-ai/dsh-credentials-local";
|
|
44
|
+
import AuthorizationService from "@deepseek-ai/dsh-authorization";
|
|
45
|
+
import * as ShellEnv from "@deepseek-ai/dsh-shell-env";
|
|
46
|
+
import * as FsObservationPolicy from "@deepseek-ai/dsh-fs-observation-policy";
|
|
47
|
+
import * as ToolFs from "@deepseek-ai/dsh-tool-fs";
|
|
48
|
+
import * as ToolBash from "@deepseek-ai/dsh-tool-bash";
|
|
49
|
+
import * as ToolTodo from "@deepseek-ai/dsh-tool-todo";
|
|
50
|
+
import * as ToolFsSearch from "@deepseek-ai/dsh-tool-fs-search";
|
|
51
|
+
import * as ToolWeb from "@deepseek-ai/dsh-tool-web";
|
|
52
|
+
import * as WebSearchDeepseek from "@deepseek-ai/dsh-web-search-deepseek";
|
|
53
|
+
import * as AnchoredToolBootstrap from "./vendor/anchored-tool-bootstrap.mjs";
|
|
54
|
+
// Fork of @deepseek-ai/dsh-code-runtime-worker-thread (same version): strips TypeScript
|
|
55
|
+
// types via amaro under Bun. Upstream takes no PRs; see github.com/nyssance/dsh-code-runtime-worker-thread.
|
|
56
|
+
import CodeRuntimeWorker from "@nyssance/dsh-code-runtime-worker-thread";
|
|
57
|
+
import CordisHostRunner from "@deepseek-ai/dsh-cordis-host-runner";
|
|
58
|
+
import * as ToolCordis from "@deepseek-ai/dsh-tool-cordis";
|
|
59
|
+
import SkillRegistry from "@deepseek-ai/dsh-skill";
|
|
60
|
+
import * as SkillFilesystem from "@deepseek-ai/dsh-skill-filesystem";
|
|
61
|
+
import * as ToolSkill from "@deepseek-ai/dsh-tool-skill";
|
|
62
|
+
import WebRuntime from "@deepseek-ai/dsh-web";
|
|
63
|
+
import TerminalSessionService from "@deepseek-ai/dsh-terminal";
|
|
64
|
+
import * as TerminalBash from "@deepseek-ai/dsh-terminal-bash";
|
|
65
|
+
import * as ToolBashPersistent from "@deepseek-ai/dsh-tool-bash-persistent";
|
|
66
|
+
import * as ToolStrReplaceEditor from "@deepseek-ai/dsh-tool-str-replace-editor";
|
|
67
|
+
|
|
68
|
+
export type HarnessPreset =
|
|
69
|
+
"standard" | "minimal" | "anchored" | "code" | "cordis";
|
|
70
|
+
|
|
71
|
+
/** Bundled skill root (vendored from dsh's cordis preset; provenance in THIRD_PARTY_NOTICES). */
|
|
72
|
+
const BUNDLED_SKILLS_DIR = fileURLToPath(
|
|
73
|
+
new URL("../skills/", import.meta.url),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cordis-preset persona, adapted from dsh's shipped `agent.cordis.yml`: the
|
|
78
|
+
* two paragraphs about preset directories and the roster are dropped — this
|
|
79
|
+
* deployment has no preset-authoring tree, its composition is fixed in code —
|
|
80
|
+
* and the skill pointer targets the one bundled skill that applies here.
|
|
81
|
+
*/
|
|
82
|
+
const CORDIS_PERSONA =
|
|
83
|
+
"You are a coding agent running on the DeepSeek Harness.\n\n" +
|
|
84
|
+
"You can read and extend the harness you run on. Its composition is Cordis: every capability " +
|
|
85
|
+
"is a plugin, and this session carries the Cordis toolset — inspect the live runtime, define " +
|
|
86
|
+
"dynamic plugins, and activate them.\n\n" +
|
|
87
|
+
"Load the `cordis-plugin-development` skill before defining or changing a plugin.";
|
|
88
|
+
export type PermissionMode =
|
|
89
|
+
"read-only" | "workspace-write" | "danger-full-access";
|
|
90
|
+
|
|
91
|
+
export const HARNESS_PRESETS: readonly HarnessPreset[] = [
|
|
92
|
+
"standard",
|
|
93
|
+
"minimal",
|
|
94
|
+
"anchored",
|
|
95
|
+
"code",
|
|
96
|
+
"cordis",
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export interface ResolvedComposeOptions {
|
|
100
|
+
/** Absent means no persistence — session/resume then fails loud. */
|
|
101
|
+
sessionsRoot?: string;
|
|
102
|
+
workspaceRoot: string;
|
|
103
|
+
permissionMode: PermissionMode;
|
|
104
|
+
preset: HarnessPreset;
|
|
105
|
+
/**
|
|
106
|
+
* Extra pi-ai provider routes (full upstream config shape passed through:
|
|
107
|
+
* apiKeyEnv / baseURL / api / models / compat / …). Absent means the
|
|
108
|
+
* multi-provider adapter is not mounted — DeepSeek rides its own
|
|
109
|
+
* direct-fetch adapter either way. An empty object mounts the adapter with
|
|
110
|
+
* no routes: its catalog and sign-in flows exist from the moment it mounts.
|
|
111
|
+
*/
|
|
112
|
+
piProviders?: Record<string, unknown>;
|
|
113
|
+
/**
|
|
114
|
+
* The harness credential file (`dsh-credentials-local`). Present whenever
|
|
115
|
+
* the pi-ai adapter is: a route declaring no apiKeyEnv authenticates through
|
|
116
|
+
* the `llm-pi-ai/<provider>` record a subscription login committed here,
|
|
117
|
+
* and pi-ai refreshes it in place.
|
|
118
|
+
*/
|
|
119
|
+
credentialsFile?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PluginRow {
|
|
123
|
+
/** Stable id = dsh package short name (package minus the @deepseek-ai/dsh- prefix). */
|
|
124
|
+
id: string;
|
|
125
|
+
package: string;
|
|
126
|
+
description: string;
|
|
127
|
+
/** Core rows keep the runtime coherent and cannot be disabled. */
|
|
128
|
+
core: boolean;
|
|
129
|
+
/** Ids of toggleable rows this row's injected services come from. */
|
|
130
|
+
requires?: string[];
|
|
131
|
+
/** Default config for this row under the resolved options (listing + mount input). */
|
|
132
|
+
config?: Record<string, unknown>;
|
|
133
|
+
/** Curated fields the desktop may edit without exposing arbitrary JSON. */
|
|
134
|
+
configurable?: PluginConfigField[];
|
|
135
|
+
/** Mounts the row; `config` is the effective (override-merged) config. */
|
|
136
|
+
mount: (
|
|
137
|
+
ctx: Context,
|
|
138
|
+
config: Record<string, unknown> | undefined,
|
|
139
|
+
) => Promise<unknown>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface PluginConfigField {
|
|
143
|
+
key: string;
|
|
144
|
+
type: "number" | "url";
|
|
145
|
+
minimum?: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function core(
|
|
149
|
+
id: string,
|
|
150
|
+
pkg: string,
|
|
151
|
+
description: string,
|
|
152
|
+
mount: PluginRow["mount"],
|
|
153
|
+
config?: Record<string, unknown>,
|
|
154
|
+
configurable?: PluginConfigField[],
|
|
155
|
+
): PluginRow {
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
package: pkg,
|
|
159
|
+
description,
|
|
160
|
+
core: true,
|
|
161
|
+
config,
|
|
162
|
+
configurable,
|
|
163
|
+
mount,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tool(
|
|
168
|
+
id: string,
|
|
169
|
+
pkg: string,
|
|
170
|
+
description: string,
|
|
171
|
+
mount: PluginRow["mount"],
|
|
172
|
+
config?: Record<string, unknown>,
|
|
173
|
+
requires?: string[],
|
|
174
|
+
configurable?: PluginConfigField[],
|
|
175
|
+
): PluginRow {
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
package: pkg,
|
|
179
|
+
description,
|
|
180
|
+
core: false,
|
|
181
|
+
config,
|
|
182
|
+
requires,
|
|
183
|
+
configurable,
|
|
184
|
+
mount,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The composition for one preset, in mount order. */
|
|
189
|
+
export function pluginRows(options: ResolvedComposeOptions): PluginRow[] {
|
|
190
|
+
const { preset, permissionMode, workspaceRoot } = options;
|
|
191
|
+
const rows: PluginRow[] = [
|
|
192
|
+
core(
|
|
193
|
+
"llm",
|
|
194
|
+
"@deepseek-ai/dsh-llm",
|
|
195
|
+
"LLM runtime: adapter registry, model resolution, streaming",
|
|
196
|
+
async (ctx, config) => ctx.plugin(LlmRuntime, config as never),
|
|
197
|
+
),
|
|
198
|
+
core(
|
|
199
|
+
"session",
|
|
200
|
+
"@deepseek-ai/dsh-session",
|
|
201
|
+
"Append-only session event log (model-visible ⟺ recorded)",
|
|
202
|
+
async (ctx, config) => ctx.plugin(SessionStore, config as never),
|
|
203
|
+
),
|
|
204
|
+
// Since dsh 0.1.2 the agent loop, sandbox policy and permission presets all
|
|
205
|
+
// inject the projection registry (per-session derived views); without it
|
|
206
|
+
// no agent factory ever registers and every session/new fails loud.
|
|
207
|
+
core(
|
|
208
|
+
"session-projection",
|
|
209
|
+
"@deepseek-ai/dsh-session-projection",
|
|
210
|
+
"Session projection registry (derived per-session views)",
|
|
211
|
+
async (ctx, config) => ctx.plugin(SessionProjectionRegistry, config as never),
|
|
212
|
+
),
|
|
213
|
+
// The anchored preset's phase 1 is persona-only, so the persona must be the
|
|
214
|
+
// exact Minimal one-liner (dsh-persona is an agent-scope shadow and collides
|
|
215
|
+
// process-wide; configuring the prompt runtime directly is the host-plane way).
|
|
216
|
+
core(
|
|
217
|
+
"system-prompt",
|
|
218
|
+
"@deepseek-ai/dsh-system-prompt",
|
|
219
|
+
"System prompt assembly (persona + tool sections)",
|
|
220
|
+
async (ctx, config) => ctx.plugin(SystemPrompt, config as never),
|
|
221
|
+
preset === "anchored"
|
|
222
|
+
? { persona: "You are a helpful software engineer assistant." }
|
|
223
|
+
: preset === "cordis"
|
|
224
|
+
? { persona: CORDIS_PERSONA }
|
|
225
|
+
: undefined,
|
|
226
|
+
),
|
|
227
|
+
// Context-global presentation is the tools row's `mode` field (presentAs is
|
|
228
|
+
// the per-agent-scope variant used by dsh's preset realms).
|
|
229
|
+
core(
|
|
230
|
+
"tools",
|
|
231
|
+
"@deepseek-ai/dsh-tools",
|
|
232
|
+
"Tool runtime: schema registry, dispatch, presentation mode",
|
|
233
|
+
async (ctx, config) => ctx.plugin(ToolRuntime, config as never),
|
|
234
|
+
preset === "code" ? { mode: "ptc" } : undefined,
|
|
235
|
+
),
|
|
236
|
+
core(
|
|
237
|
+
"agent",
|
|
238
|
+
"@deepseek-ai/dsh-agent",
|
|
239
|
+
"Agent registry (definitions and scopes)",
|
|
240
|
+
async (ctx, config) => ctx.plugin(AgentRegistry, config as never),
|
|
241
|
+
),
|
|
242
|
+
core(
|
|
243
|
+
"agent-loop",
|
|
244
|
+
"@deepseek-ai/dsh-agent-loop",
|
|
245
|
+
"The turn loop: prompt → model → tools → settlement",
|
|
246
|
+
async (ctx, config) => ctx.plugin(AgentLoop, config as never),
|
|
247
|
+
{ agents: [], maxParallelToolCalls: 10 },
|
|
248
|
+
[{ key: "maxParallelToolCalls", type: "number", minimum: 1 }],
|
|
249
|
+
),
|
|
250
|
+
core(
|
|
251
|
+
"llm-deepseek",
|
|
252
|
+
"@deepseek-ai/dsh-llm-deepseek",
|
|
253
|
+
"DeepSeek model adapter ($DEEPSEEK_API_KEY)",
|
|
254
|
+
async (ctx, config) => ctx.plugin(LlmDeepseek, config as never),
|
|
255
|
+
),
|
|
256
|
+
];
|
|
257
|
+
if (options.credentialsFile !== undefined) {
|
|
258
|
+
// The credential plane the pi-ai adapter signs into and reads from:
|
|
259
|
+
// records (`llm-pi-ai/<provider>` grants) live in this private file, and
|
|
260
|
+
// the authorization registry is where the adapter offers its logins.
|
|
261
|
+
// `watch` stays on in the server: the login CLI is a separate process
|
|
262
|
+
// writing the same file, and a fresh grant must reach a running session.
|
|
263
|
+
rows.push(
|
|
264
|
+
core(
|
|
265
|
+
"credentials-local",
|
|
266
|
+
"@deepseek-ai/dsh-credentials-local",
|
|
267
|
+
"Local credential store (subscription grants, API keys)",
|
|
268
|
+
async (ctx, config) => ctx.plugin(LocalCredentialProvider, config as never),
|
|
269
|
+
{ path: options.credentialsFile },
|
|
270
|
+
),
|
|
271
|
+
core(
|
|
272
|
+
"authorization",
|
|
273
|
+
"@deepseek-ai/dsh-authorization",
|
|
274
|
+
"Authorization flow registry (provider sign-ins)",
|
|
275
|
+
async (ctx, config) => ctx.plugin(AuthorizationService, config as never),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (options.piProviders !== undefined) {
|
|
280
|
+
rows.push(
|
|
281
|
+
core(
|
|
282
|
+
"llm-pi-ai",
|
|
283
|
+
"@deepseek-ai/dsh-llm-pi-ai",
|
|
284
|
+
"Multi-provider adapter (pi-ai catalog: openai / anthropic / google / …)",
|
|
285
|
+
async (ctx, config) => ctx.plugin(LlmPiAi, config as never),
|
|
286
|
+
{ providers: options.piProviders },
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (options.sessionsRoot !== undefined) {
|
|
291
|
+
rows.push(
|
|
292
|
+
core(
|
|
293
|
+
"session-persistence-jsonl",
|
|
294
|
+
"@deepseek-ai/dsh-session-persistence-jsonl",
|
|
295
|
+
"JSONL session logs (session/resume source)",
|
|
296
|
+
async (ctx, config) =>
|
|
297
|
+
ctx.plugin(JsonlSessionPersistence, config as never),
|
|
298
|
+
{ root: options.sessionsRoot },
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
rows.push(
|
|
303
|
+
// Execution world, sandbox-first as in dsh's shipped base bundle: commands
|
|
304
|
+
// run inside the OS sandbox; a denial asks the user only when the model
|
|
305
|
+
// escalates with sandbox_permissions. Configs mirror the bundle rows.
|
|
306
|
+
core(
|
|
307
|
+
"subprocess-local",
|
|
308
|
+
"@deepseek-ai/dsh-subprocess-local",
|
|
309
|
+
"Local subprocess runtime",
|
|
310
|
+
async (ctx, config) =>
|
|
311
|
+
ctx.plugin(LocalSubprocessRuntime, config as never),
|
|
312
|
+
),
|
|
313
|
+
core(
|
|
314
|
+
"sandbox-local",
|
|
315
|
+
"@deepseek-ai/dsh-sandbox-local",
|
|
316
|
+
"OS sandbox provider (seatbelt on macOS)",
|
|
317
|
+
async (ctx, config) => ctx.plugin(LocalSandboxProvider, config as never),
|
|
318
|
+
),
|
|
319
|
+
core(
|
|
320
|
+
"sandbox-policy",
|
|
321
|
+
"@deepseek-ai/dsh-sandbox-policy",
|
|
322
|
+
"Sandbox policy (mode + workspace root)",
|
|
323
|
+
async (ctx, config) => ctx.plugin(SandboxPolicyService, config as never),
|
|
324
|
+
{ mode: permissionMode, workspaceRoot },
|
|
325
|
+
),
|
|
326
|
+
core(
|
|
327
|
+
"bash-sandbox",
|
|
328
|
+
"@deepseek-ai/dsh-bash-sandbox",
|
|
329
|
+
"Sandboxed bash executor",
|
|
330
|
+
async (ctx, config) => ctx.plugin(SandboxBashExecutor, config as never),
|
|
331
|
+
{ timeoutMs: 60000, maxOutputBytes: 64000 },
|
|
332
|
+
[
|
|
333
|
+
{ key: "timeoutMs", type: "number", minimum: 1 },
|
|
334
|
+
{ key: "maxOutputBytes", type: "number", minimum: 1 },
|
|
335
|
+
],
|
|
336
|
+
),
|
|
337
|
+
core(
|
|
338
|
+
"fs-sandbox",
|
|
339
|
+
"@deepseek-ai/dsh-fs-sandbox",
|
|
340
|
+
"Sandbox-policied filesystem service",
|
|
341
|
+
async (ctx, config) => ctx.plugin(SandboxedFileSystem, config as never),
|
|
342
|
+
),
|
|
343
|
+
core(
|
|
344
|
+
"jobs-local",
|
|
345
|
+
"@deepseek-ai/dsh-jobs-local",
|
|
346
|
+
"Local background job registry",
|
|
347
|
+
async (ctx, config) => ctx.plugin(LocalJobRegistry, config as never),
|
|
348
|
+
),
|
|
349
|
+
core(
|
|
350
|
+
"shell-env",
|
|
351
|
+
"@deepseek-ai/dsh-shell-env",
|
|
352
|
+
"Shell environment capture for subprocesses",
|
|
353
|
+
async (ctx, config) => ctx.plugin(ShellEnv, config as never),
|
|
354
|
+
),
|
|
355
|
+
core(
|
|
356
|
+
"fs-observation-policy",
|
|
357
|
+
"@deepseek-ai/dsh-fs-observation-policy",
|
|
358
|
+
"Read-before-write observation policy",
|
|
359
|
+
async (ctx, config) => ctx.plugin(FsObservationPolicy, config as never),
|
|
360
|
+
),
|
|
361
|
+
// One-shot approvals ('ask' pairs with read-only/workspace-write presets);
|
|
362
|
+
// the ACP bridge answers them over the wire.
|
|
363
|
+
core(
|
|
364
|
+
"user-approval",
|
|
365
|
+
"@deepseek-ai/dsh-user-approval",
|
|
366
|
+
"One-shot escalation approvals (bridged over ACP)",
|
|
367
|
+
async (ctx, config) => ctx.plugin(ApprovalService, config as never),
|
|
368
|
+
{ policy: permissionMode === "danger-full-access" ? "never" : "ask" },
|
|
369
|
+
),
|
|
370
|
+
core(
|
|
371
|
+
"permission-presets",
|
|
372
|
+
"@deepseek-ai/dsh-permission-presets",
|
|
373
|
+
"Named permission presets (sandbox × approval)",
|
|
374
|
+
async (ctx, config) =>
|
|
375
|
+
ctx.plugin(PermissionPresetService, config as never),
|
|
376
|
+
{
|
|
377
|
+
presets: {
|
|
378
|
+
"read-only": { sandbox: "read-only", approval: "ask" },
|
|
379
|
+
"workspace-write": { sandbox: "workspace-write", approval: "ask" },
|
|
380
|
+
"danger-full-access": {
|
|
381
|
+
sandbox: "danger-full-access",
|
|
382
|
+
approval: "never",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
// Model-facing tools per preset (configs mirror dsh's shipped rows).
|
|
389
|
+
const terminalTrio: PluginRow[] = [
|
|
390
|
+
tool(
|
|
391
|
+
"terminal",
|
|
392
|
+
"@deepseek-ai/dsh-terminal",
|
|
393
|
+
"Persistent terminal session service",
|
|
394
|
+
async (ctx, config) =>
|
|
395
|
+
ctx.plugin(TerminalSessionService, config as never),
|
|
396
|
+
),
|
|
397
|
+
tool(
|
|
398
|
+
"terminal-bash",
|
|
399
|
+
"@deepseek-ai/dsh-terminal-bash",
|
|
400
|
+
"Bash over the persistent terminal",
|
|
401
|
+
async (ctx, config) => ctx.plugin(TerminalBash, config as never),
|
|
402
|
+
undefined,
|
|
403
|
+
["terminal"],
|
|
404
|
+
),
|
|
405
|
+
tool(
|
|
406
|
+
"tool-bash-persistent",
|
|
407
|
+
"@deepseek-ai/dsh-tool-bash-persistent",
|
|
408
|
+
"bash tool (persistent shell)",
|
|
409
|
+
async (ctx, config) => ctx.plugin(ToolBashPersistent, config as never),
|
|
410
|
+
undefined,
|
|
411
|
+
["terminal", "terminal-bash"],
|
|
412
|
+
),
|
|
413
|
+
tool(
|
|
414
|
+
"tool-str-replace-editor",
|
|
415
|
+
"@deepseek-ai/dsh-tool-str-replace-editor",
|
|
416
|
+
"str_replace_editor tool (view/create/str_replace)",
|
|
417
|
+
async (ctx, config) => ctx.plugin(ToolStrReplaceEditor, config as never),
|
|
418
|
+
{ maxOutputChars: 16000 },
|
|
419
|
+
),
|
|
420
|
+
];
|
|
421
|
+
const codingTools: PluginRow[] = [
|
|
422
|
+
tool(
|
|
423
|
+
"tool-fs",
|
|
424
|
+
"@deepseek-ai/dsh-tool-fs",
|
|
425
|
+
"read / write / edit file tools",
|
|
426
|
+
async (ctx, config) => ctx.plugin(ToolFs, config as never),
|
|
427
|
+
),
|
|
428
|
+
tool(
|
|
429
|
+
"tool-fs-search",
|
|
430
|
+
"@deepseek-ai/dsh-tool-fs-search",
|
|
431
|
+
"glob / grep search tools",
|
|
432
|
+
async (ctx, config) => ctx.plugin(ToolFsSearch, config as never),
|
|
433
|
+
{ sampleOverCapGlobResults: false },
|
|
434
|
+
),
|
|
435
|
+
tool(
|
|
436
|
+
"tool-todo",
|
|
437
|
+
"@deepseek-ai/dsh-tool-todo",
|
|
438
|
+
"todo_write plan tool",
|
|
439
|
+
async (ctx, config) => ctx.plugin(ToolTodo, config as never),
|
|
440
|
+
{ allowParallelInProgress: true },
|
|
441
|
+
),
|
|
442
|
+
// Web search rides the same DeepSeek key; fetch stays off as in the
|
|
443
|
+
// shipped bundle (tool-web { fetch: false }).
|
|
444
|
+
tool(
|
|
445
|
+
"web",
|
|
446
|
+
"@deepseek-ai/dsh-web",
|
|
447
|
+
"Web runtime (search provider registry)",
|
|
448
|
+
async (ctx, config) => ctx.plugin(WebRuntime, config as never),
|
|
449
|
+
{ searchProvider: "deepseek-official" },
|
|
450
|
+
),
|
|
451
|
+
tool(
|
|
452
|
+
"web-search-deepseek",
|
|
453
|
+
"@deepseek-ai/dsh-web-search-deepseek",
|
|
454
|
+
"DeepSeek official web search provider",
|
|
455
|
+
async (ctx, config) => ctx.plugin(WebSearchDeepseek, config as never),
|
|
456
|
+
{
|
|
457
|
+
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
458
|
+
baseURL: "https://api.deepseek.com/anthropic/v1",
|
|
459
|
+
maxUses: 5,
|
|
460
|
+
},
|
|
461
|
+
["web"],
|
|
462
|
+
[
|
|
463
|
+
{ key: "baseURL", type: "url" },
|
|
464
|
+
{ key: "maxUses", type: "number", minimum: 1 },
|
|
465
|
+
],
|
|
466
|
+
),
|
|
467
|
+
tool(
|
|
468
|
+
"tool-web",
|
|
469
|
+
"@deepseek-ai/dsh-tool-web",
|
|
470
|
+
"web_search tool",
|
|
471
|
+
async (ctx, config) => ctx.plugin(ToolWeb, config as never),
|
|
472
|
+
{ fetch: false, searchTimeoutMs: 60000 },
|
|
473
|
+
["web", "web-search-deepseek"],
|
|
474
|
+
),
|
|
475
|
+
];
|
|
476
|
+
if (preset === "minimal") {
|
|
477
|
+
// dsh's minimal preset: a two-tool coding agent — persistent bash + str_replace_editor.
|
|
478
|
+
rows.push(...terminalTrio);
|
|
479
|
+
} else if (preset === "anchored") {
|
|
480
|
+
// Community "anchored standard" (dsh-liangshen / xiaobright): the FIRST
|
|
481
|
+
// model request sees only the Minimal two-tool surface plus a one-line
|
|
482
|
+
// persona (DeepSeek V4 Pro conditions its trajectory on the first-request
|
|
483
|
+
// tool catalog: Minimal 99/96 vs Standard 91/92 in the community eval);
|
|
484
|
+
// after the anchor the full native catalog and prompt sections open.
|
|
485
|
+
rows.push(
|
|
486
|
+
...terminalTrio, // registers "bash"; the ephemeral tool-bash stays out (name collision)
|
|
487
|
+
...codingTools,
|
|
488
|
+
tool(
|
|
489
|
+
"anchored-tool-bootstrap",
|
|
490
|
+
"dsh-liangshen (vendored)",
|
|
491
|
+
"Anchored two-phase tool bootstrap",
|
|
492
|
+
async (ctx, config) =>
|
|
493
|
+
ctx.plugin(AnchoredToolBootstrap, config as never),
|
|
494
|
+
{
|
|
495
|
+
shellTools: ["bash"],
|
|
496
|
+
commonTools: ["str_replace_editor"],
|
|
497
|
+
messageSources: ["user"],
|
|
498
|
+
anchorGate: true,
|
|
499
|
+
maxBootstrapSteps: 4,
|
|
500
|
+
promoteAfterFirstResponse: true,
|
|
501
|
+
bootstrapMaxTokens: 1024,
|
|
502
|
+
compactionTools: [
|
|
503
|
+
"read",
|
|
504
|
+
"write",
|
|
505
|
+
"edit",
|
|
506
|
+
"glob",
|
|
507
|
+
"grep",
|
|
508
|
+
"todo_write",
|
|
509
|
+
],
|
|
510
|
+
deferredSources: [],
|
|
511
|
+
},
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
// standard, code, cordis share the coding-agent tool surface.
|
|
516
|
+
rows.push(
|
|
517
|
+
tool(
|
|
518
|
+
"tool-bash",
|
|
519
|
+
"@deepseek-ai/dsh-tool-bash",
|
|
520
|
+
"bash tool (ephemeral, sandboxed)",
|
|
521
|
+
async (ctx, config) => ctx.plugin(ToolBash, config as never),
|
|
522
|
+
),
|
|
523
|
+
...codingTools,
|
|
524
|
+
);
|
|
525
|
+
if (preset === "code") {
|
|
526
|
+
// Code Mode presentation backed by the official worker runtime (via the
|
|
527
|
+
// @nyssance fork: Bun lacks node:module.stripTypeScriptTypes).
|
|
528
|
+
rows.push(
|
|
529
|
+
tool(
|
|
530
|
+
"code-runtime-worker-thread",
|
|
531
|
+
"@deepseek-ai/dsh-code-runtime-worker-thread",
|
|
532
|
+
"Code Mode worker runtime (run_code)",
|
|
533
|
+
async (ctx, config) => ctx.plugin(CodeRuntimeWorker, config as never),
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (preset === "cordis") {
|
|
538
|
+
// dsh's creator preset: the self-referential Cordis toolset (define /
|
|
539
|
+
// run / inspect model-written plugins in a node:vm realm). Trust
|
|
540
|
+
// boundary, not a sandbox — mirrors the preset's own header.
|
|
541
|
+
rows.push(
|
|
542
|
+
tool(
|
|
543
|
+
"cordis-host-runner",
|
|
544
|
+
"@deepseek-ai/dsh-cordis-host-runner",
|
|
545
|
+
"node:vm realm host for model-written plugins",
|
|
546
|
+
async (ctx, config) => ctx.plugin(CordisHostRunner, config as never),
|
|
547
|
+
),
|
|
548
|
+
tool(
|
|
549
|
+
"tool-cordis",
|
|
550
|
+
"@deepseek-ai/dsh-tool-cordis",
|
|
551
|
+
"cordis_define / cordis_run / cordis_inspect tools",
|
|
552
|
+
async (ctx, config) => ctx.plugin(ToolCordis, config as never),
|
|
553
|
+
undefined,
|
|
554
|
+
["cordis-host-runner"],
|
|
555
|
+
),
|
|
556
|
+
// The composition-authoring skill travels with the preset (mirrors
|
|
557
|
+
// dsh's shipped cordis preset): registry + filesystem provider over
|
|
558
|
+
// the bundled skills/ dir + the skill tool the model calls.
|
|
559
|
+
tool(
|
|
560
|
+
"skill",
|
|
561
|
+
"@deepseek-ai/dsh-skill",
|
|
562
|
+
"Skill registry (session skill catalog)",
|
|
563
|
+
async (ctx, config) => ctx.plugin(SkillRegistry, config as never),
|
|
564
|
+
),
|
|
565
|
+
tool(
|
|
566
|
+
"skill-filesystem",
|
|
567
|
+
"@deepseek-ai/dsh-skill-filesystem",
|
|
568
|
+
"Filesystem skill provider (bundled skills/)",
|
|
569
|
+
async (ctx, config) => ctx.plugin(SkillFilesystem, config as never),
|
|
570
|
+
{ customSkillDirs: [BUNDLED_SKILLS_DIR] },
|
|
571
|
+
["skill"],
|
|
572
|
+
),
|
|
573
|
+
tool(
|
|
574
|
+
"tool-skill",
|
|
575
|
+
"@deepseek-ai/dsh-tool-skill",
|
|
576
|
+
"skill tool (load bundled skill instructions)",
|
|
577
|
+
async (ctx, config) => ctx.plugin(ToolSkill, config as never),
|
|
578
|
+
undefined,
|
|
579
|
+
["skill"],
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return rows;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Every id any preset can compose — the vocabulary the overrides file may reference. */
|
|
588
|
+
export function allPluginIds(): Set<string> {
|
|
589
|
+
const ids = new Set<string>();
|
|
590
|
+
for (const preset of HARNESS_PRESETS) {
|
|
591
|
+
for (const row of pluginRows({
|
|
592
|
+
sessionsRoot: "/",
|
|
593
|
+
workspaceRoot: "/",
|
|
594
|
+
permissionMode: "workspace-write",
|
|
595
|
+
preset,
|
|
596
|
+
})) {
|
|
597
|
+
ids.add(row.id);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return ids;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** User overrides: hand-editable, also written by the `plugins` CLI. */
|
|
604
|
+
export interface PluginOverrides {
|
|
605
|
+
disabled?: string[];
|
|
606
|
+
/** Shallow-merged over the row's default config. */
|
|
607
|
+
config?: Record<string, Record<string, unknown>>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function loadPluginOverrides(file: string): PluginOverrides {
|
|
611
|
+
if (!existsSync(file)) return {};
|
|
612
|
+
let parsed: unknown;
|
|
613
|
+
try {
|
|
614
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
615
|
+
} catch (error) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`plugin overrides file ${file} is not valid JSON: ${String(error)}`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
621
|
+
throw new Error(`plugin overrides file ${file} must contain a JSON object`);
|
|
622
|
+
}
|
|
623
|
+
const overrides = parsed as PluginOverrides;
|
|
624
|
+
if (
|
|
625
|
+
overrides.disabled !== undefined &&
|
|
626
|
+
!(
|
|
627
|
+
Array.isArray(overrides.disabled) &&
|
|
628
|
+
overrides.disabled.every((id) => typeof id === "string")
|
|
629
|
+
)
|
|
630
|
+
) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`plugin overrides file ${file}: "disabled" must be an array of plugin ids`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
if (
|
|
636
|
+
overrides.config !== undefined &&
|
|
637
|
+
(typeof overrides.config !== "object" ||
|
|
638
|
+
overrides.config === null ||
|
|
639
|
+
Array.isArray(overrides.config))
|
|
640
|
+
) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`plugin overrides file ${file}: "config" must be an object keyed by plugin id`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return { disabled: overrides.disabled, config: overrides.config };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export function savePluginOverrides(
|
|
649
|
+
file: string,
|
|
650
|
+
overrides: PluginOverrides,
|
|
651
|
+
): void {
|
|
652
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
653
|
+
writeFileSync(file, `${JSON.stringify(overrides, null, 2)}\n`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** One row of the resolved composition as the host UI sees it. */
|
|
657
|
+
export interface PluginListing {
|
|
658
|
+
id: string;
|
|
659
|
+
package: string;
|
|
660
|
+
description: string;
|
|
661
|
+
core: boolean;
|
|
662
|
+
enabled: boolean;
|
|
663
|
+
requires?: string[];
|
|
664
|
+
config?: Record<string, unknown>;
|
|
665
|
+
/** Composition-owned values before user overrides. */
|
|
666
|
+
defaultConfig?: Record<string, unknown>;
|
|
667
|
+
/** User-owned values only; absent keys inherit the composition default. */
|
|
668
|
+
overrideConfig?: Record<string, unknown>;
|
|
669
|
+
configurable?: PluginConfigField[];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export interface ResolvedPlugins {
|
|
673
|
+
/** Rows to mount, in order, with effective configs. */
|
|
674
|
+
mounted: Array<{
|
|
675
|
+
row: PluginRow;
|
|
676
|
+
config: Record<string, unknown> | undefined;
|
|
677
|
+
}>;
|
|
678
|
+
/** Every row of the preset (enabled and disabled) for listing. */
|
|
679
|
+
listing: PluginListing[];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Applies overrides to a preset's rows. Fails loud on: unknown ids, disabling
|
|
684
|
+
* a core row, and disabling a row another enabled row `requires` (cordis would
|
|
685
|
+
* hang the dependent mount instead of erroring).
|
|
686
|
+
*/
|
|
687
|
+
export function resolvePlugins(
|
|
688
|
+
rows: PluginRow[],
|
|
689
|
+
overrides: PluginOverrides,
|
|
690
|
+
): ResolvedPlugins {
|
|
691
|
+
const known = allPluginIds();
|
|
692
|
+
for (const id of overrides.disabled ?? []) {
|
|
693
|
+
if (!known.has(id))
|
|
694
|
+
throw new Error(
|
|
695
|
+
`unknown plugin id "${id}" in overrides (known: ${[...known].sort().join(", ")})`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
for (const id of Object.keys(overrides.config ?? {})) {
|
|
699
|
+
if (!known.has(id))
|
|
700
|
+
throw new Error(
|
|
701
|
+
`unknown plugin id "${id}" in overrides config (known: ${[...known].sort().join(", ")})`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const disabled = new Set(overrides.disabled ?? []);
|
|
705
|
+
for (const row of rows) {
|
|
706
|
+
if (row.core && disabled.has(row.id)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`plugin "${row.id}" is a core row and cannot be disabled`,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const presentIds = new Set(rows.map((row) => row.id));
|
|
713
|
+
for (const row of rows) {
|
|
714
|
+
if (disabled.has(row.id)) continue;
|
|
715
|
+
for (const dependency of row.requires ?? []) {
|
|
716
|
+
if (disabled.has(dependency) || !presentIds.has(dependency)) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`plugin "${row.id}" requires "${dependency}"; disable "${row.id}" too or re-enable "${dependency}"`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const effectiveConfig = (
|
|
724
|
+
row: PluginRow,
|
|
725
|
+
): Record<string, unknown> | undefined => {
|
|
726
|
+
const override = overrides.config?.[row.id];
|
|
727
|
+
if (override === undefined) return row.config;
|
|
728
|
+
return { ...row.config, ...override };
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
mounted: rows
|
|
732
|
+
.filter((row) => !disabled.has(row.id))
|
|
733
|
+
.map((row) => ({ row, config: effectiveConfig(row) })),
|
|
734
|
+
listing: rows.map((row) => ({
|
|
735
|
+
id: row.id,
|
|
736
|
+
package: row.package,
|
|
737
|
+
description: row.description,
|
|
738
|
+
core: row.core,
|
|
739
|
+
enabled: !disabled.has(row.id),
|
|
740
|
+
requires: row.requires,
|
|
741
|
+
config: effectiveConfig(row),
|
|
742
|
+
defaultConfig: row.config,
|
|
743
|
+
overrideConfig: overrides.config?.[row.id],
|
|
744
|
+
configurable: row.configurable,
|
|
745
|
+
})),
|
|
746
|
+
};
|
|
747
|
+
}
|