dsh-subagent-profile 0.3.4 → 0.5.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/README.md +13 -8
- package/README.zh.md +13 -8
- package/index.mjs +99 -95
- package/lib/client.js +1288 -189
- package/lib/core/decision-trace.mjs +20 -1
- package/lib/core/delegation.mjs +3 -2
- package/lib/core/dispatch-gates.mjs +122 -9
- package/lib/core/dispatch-schema.mjs +45 -7
- package/lib/core/dispatch-tool.mjs +24 -25
- package/lib/core/evolution-advice.mjs +154 -34
- package/lib/core/evolution-assets.mjs +167 -0
- package/lib/core/evolution-canary.mjs +427 -0
- package/lib/core/evolution-draft.mjs +333 -0
- package/lib/core/evolution-engine.mjs +413 -0
- package/lib/core/evolution-generate.mjs +180 -0
- package/lib/core/evolution-lag.mjs +78 -0
- package/lib/core/evolution-ledger.mjs +135 -27
- package/lib/core/evolution-persistence.mjs +213 -0
- package/lib/core/evolution-renewal.mjs +64 -0
- package/lib/core/evolution-routes.mjs +234 -0
- package/lib/core/http-helpers.mjs +35 -0
- package/lib/core/http-routes.mjs +35 -39
- package/lib/core/model-policy.mjs +77 -0
- package/lib/core/presets-sync.mjs +24 -2
- package/lib/core/profile-directory.mjs +17 -0
- package/lib/core/profile-provider.mjs +9 -3
- package/lib/core/profiles-query.mjs +178 -0
- package/lib/core/profiles-store.mjs +56 -8
- package/lib/core/pure.mjs +1 -1
- package/lib/core/session-read.mjs +17 -0
- package/lib/core/shims.mjs +2 -1
- package/package.json +2 -2
- package/presets/orchestrator-v2/NOTICE +7 -0
- package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
- package/presets/orchestrator-v2/custom-bash.mjs +213 -0
- package/presets/orchestrator-v2/preset.yml +2 -0
- package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
- package/presets/orchestrator/preset.yml +0 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* custom-bash — a Windows-capable `bash` tool for the liangshen preset. It
|
|
3
|
+
* registers under the SAME name (`bash`) as the persistent shell so the
|
|
4
|
+
* phase-1 Minimal anchor (bash + str_replace_editor) holds on every platform,
|
|
5
|
+
* but executes through `ctx.subprocess.spawn` instead of a PTY.
|
|
6
|
+
*
|
|
7
|
+
* WHY: DSH's PTY backend is linux/darwin-only — subprocess-local throws
|
|
8
|
+
* "terminal inspection is unsupported on platform win32", so the
|
|
9
|
+
* persistent-shell group (dsh-terminal + dsh-terminal-bash +
|
|
10
|
+
* dsh-tool-bash-persistent) cannot spawn on Windows. agent.cordis.yml disables
|
|
11
|
+
* that group on win32 and enables this tool instead; both platforms end up
|
|
12
|
+
* with exactly one `bash` tool and the byte-exact schema anchor is kept.
|
|
13
|
+
*
|
|
14
|
+
* Executable resolution (config `bashPath`, no hardcoded install path):
|
|
15
|
+
* an explicit non-empty `bashPath` wins unconditionally. Unset, the Git Bash
|
|
16
|
+
* executable is INFERRED in probe order:
|
|
17
|
+
* 1. the `git` executable on PATH — its install root carries `bin\bash.exe`
|
|
18
|
+
* one level up from `cmd\`, beside `bin\`, or two levels up from
|
|
19
|
+
* `mingw64\bin\` (the standard installer, choco, and winget all resolve
|
|
20
|
+
* here);
|
|
21
|
+
* 2. the well-known Git-for-Windows roots derived from environment variables
|
|
22
|
+
* (ProgramFiles, ProgramFiles(x86), LOCALAPPDATA\Programs\Git, scoop's
|
|
23
|
+
* ~\scoop\apps\git\current junction);
|
|
24
|
+
* 3. plain `bash` through `ctx.subprocess.resolveExecutable` (PATH lookup —
|
|
25
|
+
* last resort; on Windows that may pick the WSL shim, which is still true
|
|
26
|
+
* bash even though paths shift to /mnt/…).
|
|
27
|
+
*
|
|
28
|
+
* If NOTHING resolves the tool fails with an actionable error — it never
|
|
29
|
+
* silently executes under pwsh/cmd, which are different command languages.
|
|
30
|
+
*
|
|
31
|
+
* Semantics mirror the official bash tool: `bash -c <command>` in a fresh
|
|
32
|
+
* process, bounded output, non-zero exit reported not thrown. No sandbox
|
|
33
|
+
* confinement on Windows (the sandbox backend is linux-only); the description
|
|
34
|
+
* says so.
|
|
35
|
+
*
|
|
36
|
+
* Ported from xiaobright/dsh-anchored-standard (MIT) — see NOTICE.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { access } from 'node:fs/promises'
|
|
40
|
+
import { dirname, join } from 'node:path'
|
|
41
|
+
|
|
42
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
43
|
+
export const name = 'custom-bash'
|
|
44
|
+
|
|
45
|
+
/** The subprocess and tools services must exist before this tool can register. */
|
|
46
|
+
export const inject = ['subprocess', 'tools']
|
|
47
|
+
|
|
48
|
+
const DEFAULT_TIMEOUT_MS = 120000
|
|
49
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 64000
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Git Bash candidate paths, in probe order (see the header): the `git`
|
|
53
|
+
* executable's install root first, then the well-known env-derived roots.
|
|
54
|
+
* Exported for tests; pure — existence probing happens at the call site.
|
|
55
|
+
*/
|
|
56
|
+
export function bashCandidates(env, gitExe) {
|
|
57
|
+
const candidates = []
|
|
58
|
+
// git at <root>\cmd\git.exe (installer/scoop) or <root>\bin\git.exe →
|
|
59
|
+
// <root>\bin\bash.exe; <root>\mingw64\bin\git.exe (portable) → two up.
|
|
60
|
+
// A bare relative name means `git` did not actually resolve to a path.
|
|
61
|
+
if (typeof gitExe === 'string' && /[/\\]/.test(gitExe)) {
|
|
62
|
+
const dir = dirname(gitExe)
|
|
63
|
+
const root = dirname(dir)
|
|
64
|
+
candidates.push(
|
|
65
|
+
join(root, 'bin', 'bash.exe'),
|
|
66
|
+
join(dir, 'bash.exe'),
|
|
67
|
+
join(dirname(root), 'bin', 'bash.exe'),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (env.ProgramFiles) candidates.push(join(env.ProgramFiles, 'Git', 'bin', 'bash.exe'))
|
|
71
|
+
if (env['ProgramFiles(x86)']) candidates.push(join(env['ProgramFiles(x86)'], 'Git', 'bin', 'bash.exe'))
|
|
72
|
+
if (env.LOCALAPPDATA) candidates.push(join(env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe'))
|
|
73
|
+
if (env.USERPROFILE) candidates.push(join(env.USERPROFILE, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'))
|
|
74
|
+
// Layouts overlap (a `bin` git.exe derives the same bash twice) — probe
|
|
75
|
+
// order survives the dedupe, insertion order is preserved.
|
|
76
|
+
return [...new Set(candidates)]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Tool parameter schema for the model-facing command. */
|
|
80
|
+
const commandSchema = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
command: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'The bash command to execute (`bash -c` string domain).',
|
|
86
|
+
},
|
|
87
|
+
workdir: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Optional working directory; defaults to the session cwd.',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ['command'],
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Register the model-facing `bash` tool. */
|
|
97
|
+
export function apply(ctx, config) {
|
|
98
|
+
const explicitBashPath = typeof config?.bashPath === 'string' && config.bashPath.length > 0 ? config.bashPath : undefined
|
|
99
|
+
const timeoutMs = Number.isSafeInteger(config?.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS
|
|
100
|
+
const maxOutputBytes = Number.isSafeInteger(config?.maxOutputBytes) && config.maxOutputBytes > 0 ? config.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
|
|
101
|
+
|
|
102
|
+
// The inferred executable is memoized per plugin instance: candidate probing
|
|
103
|
+
// walks the filesystem, and the answer cannot change within a mount. A
|
|
104
|
+
// failed inference is NOT memoized — the plain `bash` fallback resolves
|
|
105
|
+
// fresh on every execute until some probe succeeds.
|
|
106
|
+
let inferredShell
|
|
107
|
+
const exists = (path) => access(path).then(() => true, () => false)
|
|
108
|
+
const resolveShell = async (signal) => {
|
|
109
|
+
if (explicitBashPath !== undefined) {
|
|
110
|
+
// A misconfigured explicit path must fail as itself, not as a
|
|
111
|
+
// discovery miss — the raw resolution error says which path failed.
|
|
112
|
+
return ctx.subprocess.resolveExecutable(explicitBashPath, undefined, signal)
|
|
113
|
+
}
|
|
114
|
+
if (inferredShell !== undefined) {
|
|
115
|
+
return ctx.subprocess.resolveExecutable(inferredShell, undefined, signal)
|
|
116
|
+
}
|
|
117
|
+
let gitExe
|
|
118
|
+
try {
|
|
119
|
+
gitExe = await ctx.subprocess.resolveExecutable('git', undefined, signal)
|
|
120
|
+
} catch {
|
|
121
|
+
// git unresolvable → the env-derived candidates below still apply
|
|
122
|
+
}
|
|
123
|
+
for (const candidate of bashCandidates(process.env, gitExe)) {
|
|
124
|
+
if (!(await exists(candidate))) continue
|
|
125
|
+
try {
|
|
126
|
+
inferredShell = await ctx.subprocess.resolveExecutable(candidate, undefined, signal)
|
|
127
|
+
return inferredShell
|
|
128
|
+
} catch {
|
|
129
|
+
// Exists but unresolvable (EPERM, a broken scoop junction): keep
|
|
130
|
+
// probing — one bad root must not block the rest of the chain, and
|
|
131
|
+
// nothing is memoized so later executes can still find a good one.
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return await ctx.subprocess.resolveExecutable('bash', undefined, signal)
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Total discovery failure (no Git Bash root, no env root, no bash on
|
|
139
|
+
// PATH): name the remedies instead of leaking a raw ENOENT. Never
|
|
140
|
+
// fall back to pwsh/cmd here — the schema promises `bash -c`
|
|
141
|
+
// semantics; a different shell would silently break every command.
|
|
142
|
+
throw new Error(`bash executable not found — install Git for Windows, expose a bash on PATH, or set the custom-bash \`bashPath\` config (${String((error && error.message) || error)})`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ctx.tools.register({
|
|
147
|
+
name: 'bash',
|
|
148
|
+
description: [
|
|
149
|
+
'Run commands in a bash shell (Git Bash on Windows)',
|
|
150
|
+
'* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
|
|
151
|
+
"* You don't have access to the internet via this tool.",
|
|
152
|
+
'* You do have access to a mirror of common linux and python packages via apt and pip.',
|
|
153
|
+
'* State does NOT persist across command calls: each call runs in a fresh shell.',
|
|
154
|
+
"* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
|
|
155
|
+
'* Please avoid commands that may produce a very large amount of output.',
|
|
156
|
+
'* NOTE: runs without OS sandbox confinement on Windows (no landlock); treat output as untrusted.',
|
|
157
|
+
].join('\n'),
|
|
158
|
+
parameters: commandSchema,
|
|
159
|
+
output: {
|
|
160
|
+
schema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
text: { type: 'string' },
|
|
165
|
+
},
|
|
166
|
+
required: ['text'],
|
|
167
|
+
},
|
|
168
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
169
|
+
},
|
|
170
|
+
async execute(args, exec) {
|
|
171
|
+
const shell = await resolveShell(exec?.signal)
|
|
172
|
+
const workdir = typeof args.workdir === 'string' && args.workdir.length > 0
|
|
173
|
+
? args.workdir
|
|
174
|
+
: exec?.agent?.session?.header?.cwd
|
|
175
|
+
const signal = exec?.signal
|
|
176
|
+
const handle = ctx.subprocess.spawn({
|
|
177
|
+
argv: [shell, '-c', args.command],
|
|
178
|
+
...workdir !== undefined ? { cwd: workdir } : {},
|
|
179
|
+
stdio: {
|
|
180
|
+
stdin: 'ignore',
|
|
181
|
+
stdout: { maxBytes: maxOutputBytes },
|
|
182
|
+
stderr: { maxBytes: maxOutputBytes },
|
|
183
|
+
},
|
|
184
|
+
...signal !== undefined ? { signal } : {},
|
|
185
|
+
graceMs: 3000,
|
|
186
|
+
})
|
|
187
|
+
let outcome
|
|
188
|
+
try {
|
|
189
|
+
outcome = await handle.done
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// A spawn-level failure (bad executable, EPERM) surfaces as a throw,
|
|
192
|
+
// which the runtime turns into an isError result.
|
|
193
|
+
throw new Error(`bash spawn failed: ${String(error)}`)
|
|
194
|
+
}
|
|
195
|
+
let stdout = ''
|
|
196
|
+
let stderr = ''
|
|
197
|
+
try {
|
|
198
|
+
stdout = handle.collected.stdout.readFrom(0).text
|
|
199
|
+
stderr = handle.collected.stderr.readFrom(0).text
|
|
200
|
+
} catch {
|
|
201
|
+
// Collected readers may be unavailable on some backends; tolerate.
|
|
202
|
+
}
|
|
203
|
+
const text = [stdout, stderr].filter((part) => part.length > 0).join('\n')
|
|
204
|
+
const tail = text.length > 0 ? text : `exit code: ${outcome.exitCode} (no output)`
|
|
205
|
+
if (outcome.exitCode !== 0) {
|
|
206
|
+
// Non-zero exit is a reported failure, not a throw: the model sees the
|
|
207
|
+
// command output plus the exit code.
|
|
208
|
+
throw new Error(tail)
|
|
209
|
+
}
|
|
210
|
+
return { text: tail }
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
}
|