dsh-gitbash-shell 0.39.3 → 0.40.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,195 @@
1
+ import { registerExperimentalTool } from './experimental-tools.js'
2
+ import { createNativeExecutor } from './native-executor.js'
3
+ import { processJob, processSources, ringDelta } from './bash-render.js'
4
+
5
+ const typedPath = () => ({ type: 'object', additionalProperties: false, properties: {
6
+ kind: { type: 'string', const: 'path', required: true }, value: { type: 'string', required: true },
7
+ } })
8
+
9
+ /** defineTool's AUTHOR property-map, not a raw object-root JSON schema. */
10
+ export function nativeParameters({ background = false, promote = false } = {}) {
11
+ return {
12
+ description: { type: 'string', required: true, description: 'Short description of the intended native operation.' },
13
+ executable: { ...typedPath(), required: true, description: 'Absolute native .exe path. MSYS paths translate only in kind:path values. Bare commands, scripts, .cmd and .bat are unsupported.' },
14
+ argv: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
15
+ kind: { type: 'string', enum: ['literal', 'path'], required: true }, value: { type: 'string', required: true },
16
+ } }, description: 'Each literal is passed unchanged (including /all, /c, quotes and empty strings); each path must be absolute and is explicitly translated to native Windows spelling. No shell expansion.' },
17
+ cwd: { ...typedPath(), description: 'Absolute typed working directory; omitted means the agent workspace.' },
18
+ entrypoint: { type: 'string', enum: ['native', 'interpreter'], description: 'Default native. Known interpreters require explicit interpreter. Their own command language/quoting is not made safe by direct argv; no automatic script wrapper.' },
19
+ timeoutMs: { type: 'number', description: promote ? 'Foreground wait deadline, default 120000ms, capped at 600000ms; expiry promotes the managed process to a job. Not accepted with run_in_background.' : 'Kill deadline, default 120000ms, capped at 600000ms. Not accepted with run_in_background.' },
20
+ ...(background ? { run_in_background: { type: 'boolean', description: 'Use existing managed Jobs; collect with job_output, stop with job_kill. No tool deadline. Requires an attached job controller.' } } : {}),
21
+ stdoutEncoding: { type: 'string', enum: ['utf8', 'gbk', 'utf16le', 'raw'], description: 'Known producer stdout encoding, default utf8. raw preserves unknown/mixed bytes without claiming reliable text. No auto detection.' },
22
+ stderrEncoding: { type: 'string', enum: ['utf8', 'gbk', 'utf16le', 'raw'], description: 'Known producer stderr encoding, independently selected; default utf8.' },
23
+ errors: { type: 'string', enum: ['replace', 'strict'], description: 'Default replace; strict records decoding failures separately from actual command exit. Never rerun a side-effecting command merely because output decoding failed.' },
24
+ }
25
+ }
26
+
27
+ const streamSchema = { type: 'object', additionalProperties: false, properties: {
28
+ text: { type: 'string', required: true }, truncated: { type: 'boolean', required: true }, spillPath: { type: 'string' },
29
+ } }
30
+ // Core's author DSL supports integer, not numeric minimum keywords; the
31
+ // adapter owns non-negative counters and budget validation before execution.
32
+ const byteCount = () => ({ type: 'integer', required: true })
33
+ const factBoolean = () => ({ type: 'boolean', required: true })
34
+ const outputErrorSchema = { type: 'object', additionalProperties: false, properties: {
35
+ code: { type: 'string', required: true }, message: { type: 'string', required: true },
36
+ } }
37
+ const normalizedEncoding = ['utf-8', 'gbk', 'utf-16le', 'raw']
38
+ const artifactSchema = kind => ({ type: 'object', additionalProperties: false, properties: {
39
+ kind: { type: 'string', const: kind, required: true },
40
+ encoding: { type: 'string', ...(kind === 'decoded' ? { const: 'utf-8' } : { enum: normalizedEncoding }), required: true },
41
+ path: { type: 'string' }, bytes: byteCount(), complete: factBoolean(), capReached: factBoolean(), error: outputErrorSchema,
42
+ } })
43
+ const streamFactsSchema = { type: 'object', additionalProperties: false, properties: {
44
+ encoding: { type: 'string', enum: normalizedEncoding, required: true },
45
+ errors: { type: 'string', enum: ['replace', 'strict'], required: true },
46
+ rawBytes: byteCount(), decodedUtf8Bytes: byteCount(), rawCursor: byteCount(), decodedCursor: byteCount(),
47
+ reliableText: factBoolean(), lossy: factBoolean(), complete: factBoolean(), sourceComplete: factBoolean(),
48
+ aborted: factBoolean(), settled: factBoolean(),
49
+ termination: { type: 'string', enum: ['end', 'close-before-end', 'stream-error', 'missing-pipe', 'spawn-error', 'abort-drain-timeout', 'terminate-drain-timeout', 'exit-drain-timeout', 'provider-error-drain-timeout'] },
50
+ decodeError: outputErrorSchema, streamError: outputErrorSchema, storageError: outputErrorSchema,
51
+ artifact: { type: 'object', additionalProperties: false, required: true, properties: {
52
+ raw: artifactSchema('raw'), decoded: artifactSchema('decoded'),
53
+ } },
54
+ retainedMaxBytes: byteCount(),
55
+ } }
56
+ // Explicit current adapter DTO, including the two executor-owned degradation
57
+ // branches. Optional undefined projection never admits arbitrary output fields.
58
+ export const NATIVE_OUTPUT_FACTS_SCHEMA = { oneOf: [
59
+ { type: 'object', additionalProperties: false, properties: {
60
+ stdout: { ...streamFactsSchema, required: true }, stderr: { ...streamFactsSchema, required: true },
61
+ budget: { type: 'object', additionalProperties: false, required: true, properties: {
62
+ maxTotalBytes: byteCount(), reservedMemoryBytes: byteCount(), artifactBytes: byteCount(), maxArtifactBytes: byteCount(),
63
+ } },
64
+ } },
65
+ { type: 'object', additionalProperties: false, properties: { unavailable: { type: 'boolean', const: true, required: true } } },
66
+ { type: 'object', additionalProperties: false, properties: {
67
+ projectionError: { type: 'string', required: true }, reliableText: { type: 'boolean', const: false, required: true },
68
+ } },
69
+ ] }
70
+ const sandboxSchema = { type: 'object', additionalProperties: false, properties: {
71
+ mode: { type: 'string', const: 'danger-full-access', required: true }, denied: { type: 'boolean', const: false, required: true },
72
+ } }
73
+ const encodingSchema = { type: 'object', additionalProperties: false, properties: {
74
+ stdoutEncoding: { type: 'string', enum: ['utf8', 'gbk', 'utf16le', 'raw'], required: true },
75
+ stderrEncoding: { type: 'string', enum: ['utf8', 'gbk', 'utf16le', 'raw'], required: true },
76
+ errors: { type: 'string', enum: ['replace', 'strict'], required: true },
77
+ } }
78
+ export const NATIVE_OUTPUT_SCHEMA = {
79
+ oneOf: [
80
+ { type: 'object', additionalProperties: false, properties: {
81
+ kind: { type: 'string', const: 'foreground', required: true }, cwd: { type: 'string', required: true },
82
+ status: { type: 'string', enum: ['running', 'completed', 'killed'], required: true }, exitCode: { required: true, oneOf: [{ type: 'integer' }, { type: 'null' }] },
83
+ signal: { required: true, oneOf: [{ type: 'string' }, { type: 'null' }] },
84
+ timedOut: { type: 'boolean', required: true }, aborted: { type: 'boolean', required: true }, timeoutMs: { type: 'number', required: true },
85
+ stdout: { ...streamSchema, required: true }, stderr: { ...streamSchema, required: true },
86
+ sandbox: { ...sandboxSchema, required: true }, outputFacts: { ...NATIVE_OUTPUT_FACTS_SCHEMA, required: true },
87
+ } },
88
+ { type: 'object', additionalProperties: false, properties: {
89
+ kind: { type: 'string', enum: ['background', 'promoted'], required: true }, cwd: { type: 'string', required: true },
90
+ jobId: { type: 'string', required: true }, timeoutMs: { type: 'number' }, output: { type: 'string' },
91
+ encoding: { ...encodingSchema, required: true },
92
+ } },
93
+ ],
94
+ }
95
+
96
+ export function renderNativeResult(value) {
97
+ if (value.kind !== 'foreground') return `Native command ${value.kind === 'promoted' ? 'continues after the wait deadline' : 'admitted to managed Jobs'}; job id: ${value.jobId}. Collect with job_output, stop with job_kill.\n${value.output ?? ''}\n[requested encoding: ${JSON.stringify(value.encoding)}; final producer/output facts arrive in job output]`
98
+ const text = value.stdout.text + (value.stderr.text ? `\n[stderr]\n${value.stderr.text}` : '')
99
+ return `${text}\n[exit code: ${value.exitCode ?? 'unknown'}; status: ${value.status}; signal: ${value.signal ?? 'none'}; timedOut: ${value.timedOut}; aborted: ${value.aborted}]\n[native output facts: ${JSON.stringify(value.outputFacts)}]\nOutput decoding/storage problems do not mean the command did not execute; inspect producer exit facts before considering any retry.`
100
+ }
101
+
102
+ /** One per-agent module. Host packages and the shared path translator are injected. */
103
+ export function createNativeToolModule(deps) {
104
+ if (typeof deps.toolsModule?.defineTool !== 'function') throw new Error('gitbash_native: host defineTool capability unavailable')
105
+ return {
106
+ inject: ['tools', 'subprocess', 'shellEnv'],
107
+ apply(ctx, config = {}) {
108
+ const executor = createNativeExecutor(ctx, deps)
109
+ const enabled = config.enableRunInBackground !== false
110
+ const promoteOnTimeout = config.promoteOnTimeout !== false && enabled
111
+ const definition = jobs => {
112
+ const background = enabled && jobs !== undefined
113
+ const promote = background && promoteOnTimeout
114
+ const startJob = (request, exec) => {
115
+ let proc
116
+ let finalFacts = ''
117
+ const sources = processSources(() => proc)
118
+ sources.push({ channel: 'stderr', read(fromByte) {
119
+ const bytes = Buffer.from(finalFacts, 'utf8')
120
+ return { text: bytes.subarray(fromByte).toString('utf8'), nextOffset: bytes.length, lossy: false }
121
+ } })
122
+ const id = jobs.start({ kind: 'gitbash-native', label: request.description,
123
+ ...(exec.agent ? { owner: exec.agent.id } : {}), output: sources,
124
+ run: () => processJob(async signal => {
125
+ proc = await executor.execute(request, exec, { signal, onExpiry: 'none' })
126
+ return proc
127
+ }, async started => {
128
+ // done has already sealed decoding and released this operation's scope.
129
+ const result = await started.result()
130
+ finalFacts = `\n[native producer: exitCode=${result.exitCode}, signal=${result.signal}, status=${result.status}, aborted=${result.aborted}]\n[native output facts: ${JSON.stringify(result.outputFacts)}]\nOutput failure is not command non-execution; do not blindly retry.\n`
131
+ return { status: result.status === 'killed' ? 'killed' : 'completed', detail: `exit code: ${result.exitCode ?? 'unknown'}; output facts retained in job output` }
132
+ }),
133
+ })
134
+ return { id, process: () => proc }
135
+ }
136
+ const execute = async (args, exec) => {
137
+ // prepare resolves policy BEFORE ensure/path/env/artifact work.
138
+ const request = await executor.prepare(args, exec)
139
+ exec.signal?.throwIfAborted()
140
+ if (request.run_in_background && !background) throw new Error('run_in_background is unavailable or disabled; this request was not executed')
141
+ if (request.run_in_background) {
142
+ const attached = startJob(request, exec)
143
+ return { kind: 'background', jobId: attached.id, cwd: request.cwd, encoding: request.output }
144
+ }
145
+ if (promote) {
146
+ // No retry/fallback around admission: a host throwing after starting a
147
+ // producer must not cause a second execution of a side-effecting call.
148
+ const attached = startJob(request, exec)
149
+ const owner = exec.agent?.id
150
+ let view
151
+ try { view = await jobs.wait(attached.id, executor.waitBudget(request), owner, exec.signal) }
152
+ catch (error) {
153
+ jobs.kill(attached.id, owner, 'tool call aborted')
154
+ await jobs.wait(attached.id, 10000, owner)
155
+ throw error
156
+ }
157
+ if (view.status === 'running' || view.status === 'stopping') {
158
+ if (!attached.process()) {
159
+ jobs.kill(attached.id, owner, 'timed out during native preparation')
160
+ await jobs.wait(attached.id, 10000, owner)
161
+ throw new Error(`native preparation timed out; job ${attached.id} was cancelled, inspect job_output before retrying`)
162
+ }
163
+ const read = jobs.read(attached.id, owner)
164
+ return { kind: 'promoted', jobId: attached.id, cwd: request.cwd, timeoutMs: request.timeoutMs,
165
+ output: ringDelta(read.chunks), encoding: request.output }
166
+ }
167
+ jobs.remove(attached.id, owner)
168
+ if (!attached.process()) throw new Error(view.detail ?? 'native preparation failed before a process handle was available')
169
+ return { kind: 'foreground', ...await attached.process().result() }
170
+ }
171
+ const proc = await executor.execute(request, exec)
172
+ return { kind: 'foreground', ...await proc.result() }
173
+ }
174
+ return deps.toolsModule.defineTool({ name: 'gitbash_native',
175
+ description: 'Choose this tool BEFORE execution for a Windows-native .exe that needs exact literal argv (for example /all or /c without MSYS conversion) or explicitly known independent stdout/stderr encodings. Use an offered shell tool instead for pipelines, redirection, globbing, shell expansion or POSIX scripts; this is direct typed argv through the host managed executor, not shell source. Supply an absolute executable, mark literal arguments unchanged and path arguments explicitly; do not put a whole shell command into argv. Prefer a producer UTF-8 option; declare gbk/utf16le only when known, raw for unknown or mixed bytes. In PTC call this offered SDK binding from inside run_code, not as a direct model tool call. Only actual danger-full-access standing policy is supported; missing/restricted policy is rejected before execution. A policy/access denial is a stop condition, never permission for another-tool retry. Decoding/storage failure preserves producer exit: never blindly repeat side effects. No user env, implicit scripts, .cmd/.bat wrappers, path guessing or encoding auto-detection. Known interpreters require explicit entrypoint:interpreter; common CRT/Node argv is supported, arbitrary interpreter quoting is not guaranteed.',
176
+ parameters: nativeParameters({ background, promote }),
177
+ output: { schema: NATIVE_OUTPUT_SCHEMA, render: (_args, value) => [{ type: 'text', text: renderNativeResult(value) }], presentationMeta: (_args, value) => ({ cwd: value.cwd }) },
178
+ execute,
179
+ presentCall: args => ({ card: 'generic', title: args.description, kind: 'execute', content: [{ type: 'text', text: args.executable.value }] }),
180
+ })
181
+ }
182
+ let currentJobs
183
+ const refresh = registerExperimentalTool(ctx, () => definition(currentJobs), deps.experimentalEnabled, { scope: deps.scope, log: deps.log })
184
+ if (!enabled) return
185
+ ctx.inject(['jobs'], jobCtx => {
186
+ currentJobs = jobCtx.jobs
187
+ refresh()
188
+ jobCtx.effect(() => () => {
189
+ currentJobs = undefined
190
+ if (ctx.fiber?.state === 2) refresh()
191
+ })
192
+ })
193
+ },
194
+ }
195
+ }
@@ -0,0 +1,297 @@
1
+ import { registerExperimentalTool } from './experimental-tools.js'
2
+
3
+ /**
4
+ * dsh-gitbash-shell/path-tool — the EXPLICIT full-value MSYS→native converter
5
+ * (`gitbash_to_native_path`, 2026-10-13, compat-implementation task
6
+ * IMPL-PATH-HELPER).
7
+ *
8
+ * WHY. run_code hands a program's path STRINGS to native Node, and the legacy
9
+ * codePaths literal rewrite can only ever guess which literals are paths
10
+ * (research REPORT.md: route/search data still gets corrupted, dynamic values
11
+ * are out of reach by design). The explicit alternative: the CALLER decides a
12
+ * complete value is a path and converts it AT RUNTIME through one tool:
13
+ *
14
+ * const r = await tools.gitbash_to_native_path({ msys_path: p })
15
+ * await fs.readFile(r.native_path, 'utf8')
16
+ *
17
+ * Contract (mirrors the approved research fixture `fixture_to_native`):
18
+ * - the parameter is named `msys_path` ON PURPOSE: the automatic
19
+ * tools/execute path-field rewrite only touches the whitelisted
20
+ * `file_path`/`path`/`workdir` fields (src/index.js
21
+ * TRANSLATABLE_PATH_FIELDS), so the raw MSYS value reaches this tool
22
+ * intact and the conversion is the tool's own explicit job;
23
+ * - the value goes through the SHARED `translateMsysPath` + a FRESH
24
+ * `buildTranslateEnv(...)` per call (injected, never copied here) — drive
25
+ * roots, ~, $HOME-style variables, /tmp, /dev/null and the Git install
26
+ * mounts resolve exactly like every other translated face of the plugin;
27
+ * - NO filesystem I/O, NO existence check, NO permission grant, NO global
28
+ * env seeding: the caller's own runtime policy still governs what the
29
+ * converted path may touch;
30
+ * - already Windows-native (drive-letter/UNC) or cwd-relative values are
31
+ * returned unchanged — declared here, in the description;
32
+ * - a POSIX-looking value that NO mount maps, or whose needed mount fact is
33
+ * unavailable, is REJECTED with an explicit error: returning the POSIX
34
+ * spelling as `native_path` would be a silent lie;
35
+ * - empty / NUL-byte / URL-shaped values are rejected outright; Unicode,
36
+ * `#` and spaces inside a path are preserved byte for byte;
37
+ * - the tool is INDEPENDENT of the automatic dialect switches
38
+ * (posixPaths/codePaths/virtualMounts): whether it is registered at all
39
+ * (and the Windows-only gate) is the main wiring's decision — this module
40
+ * carries no Config and reads no dialect state.
41
+ *
42
+ * Mounting follows the bash-tool pattern: `createPathToolModule(deps)` returns
43
+ * a cordis module ({ inject, apply }) that the main wiring mounts inside an
44
+ * agent-owned scope; registration reverts through the scope's effect on
45
+ * disposal. There are NO top-level static `@deepseek-ai/*` imports — the host
46
+ * `defineTool` and the shared translator ride the deps bag (v0.24.3 lesson: a
47
+ * failed static peer import silently drops the whole plugin row).
48
+ *
49
+ * The SUCCESS result is `{ native_path }`, a shape the result-echo layer
50
+ * never rewrites (rewriteResultPaths is keyed on the official tools' names
51
+ * and path metadata fields, not on this tool), so the native value cannot be
52
+ * translated back behind the caller's back.
53
+ *
54
+ * @module dsh-gitbash-shell/path-tool
55
+ */
56
+
57
+ /** The registered tool name. */
58
+ export const PATH_TOOL_NAME = 'gitbash_to_native_path'
59
+
60
+ /** Windows drive-letter absolute: `C:/x`, `C:\x` (drive-relative `C:` is NOT). */
61
+ const WINDOWS_ABSOLUTE = /^[A-Za-z]:[\\/]/
62
+
63
+ /** A `scheme:` prefix — `https:`, `file:`, `app:`, … (single-letter `C:` excluded). */
64
+ const URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.\-]*):/
65
+
66
+ const NUL_BYTE = '\0'
67
+
68
+ /**
69
+ * Whether the value is ALREADY in a native Windows spelling (drive-letter
70
+ * absolute or UNC, both slash styles). Pure.
71
+ * @param {unknown} value
72
+ * @returns {boolean}
73
+ */
74
+ export function isWindowsNativeValue(value) {
75
+ return typeof value === 'string'
76
+ && (WINDOWS_ABSOLUTE.test(value) || value.startsWith('\\\\') || value.startsWith('//'))
77
+ }
78
+
79
+ /**
80
+ * Whether the value LOOKS like it needs MSYS mount resolution (POSIX root,
81
+ * home shorthand or a leading variable). A match does NOT promise a mapping
82
+ * exists — only that a passthrough would be a guess. Pure.
83
+ * @param {unknown} value
84
+ * @returns {boolean}
85
+ */
86
+ export function looksPosixValue(value) {
87
+ return typeof value === 'string'
88
+ && (value.startsWith('/') || value.startsWith('~') || value.startsWith('$'))
89
+ }
90
+
91
+ /**
92
+ * One honest line about which mount facts the conversion could see. `null`
93
+ * means the wiring turned mounts off; `undefined` means the wiring did not
94
+ * expose facts at all.
95
+ */
96
+ function factSummary(env) {
97
+ if (env === null) return 'mount facts unavailable (virtual mounts are off)'
98
+ if (!env || typeof env !== 'object') return 'mount facts not exposed by this wiring'
99
+ return `mount facts: home=${env.home ? 'yes' : 'no'} tmpDir=${env.tmpDir ? 'yes' : 'no'} gitRoot=${env.gitRoot ? 'yes' : 'no'}`
100
+ }
101
+
102
+ /**
103
+ * Convert ONE caller-declared full path value. Pure: string surgery only,
104
+ * the translator (the shared translateMsysPath composed with a fresh mount
105
+ * fact bag) is injected, nothing is probed here.
106
+ *
107
+ * Rejections are explicit and non-guessing:
108
+ * - EMPTY — not a non-empty string;
109
+ * - NUL_BYTE — a NUL byte anywhere in the value;
110
+ * - URL — a multi-letter scheme prefix (`https:…`, `file:…`);
111
+ * - NO_MOUNT_MAPPING — POSIX-looking but the shared mount table returned it
112
+ * unchanged: either no mount maps it or a needed fact is missing. The
113
+ * POSIX spelling is NEVER returned as `native_path`.
114
+ *
115
+ * @param {unknown} value - the complete value the caller has decided is a path.
116
+ * @param {{ translate?: (value: string, env?: object|null) => unknown,
117
+ * env?: object|null }} [options] - the injected shared translator
118
+ * and this call's fresh mount-fact bag (for the rejection diagnostics).
119
+ * @returns {{ ok: true, native_path: string,
120
+ * status: 'converted'|'passthrough', kind: string }
121
+ * | { ok: false, code: string, message: string }}
122
+ */
123
+ export function convertMsysToNative(value, { translate, env } = {}) {
124
+ // The code rides the message: the registry carries only error.message to
125
+ // the model/bridge, so the rejection class must survive that one face.
126
+ const reject = (code, body) => ({ ok: false, code, message: `${PATH_TOOL_NAME} [${code}]: ${body}` })
127
+ if (typeof value !== 'string' || value.trim().length === 0) {
128
+ return reject('EMPTY', 'msys_path must be a non-empty string')
129
+ }
130
+ if (value.includes(NUL_BYTE)) {
131
+ return reject('NUL_BYTE', 'msys_path contains a NUL byte; a single filesystem path is required')
132
+ }
133
+ const scheme = URL_SCHEME.exec(value)
134
+ if (scheme && scheme[1].length >= 2) {
135
+ return reject('URL', `msys_path looks like a URL (scheme '${scheme[1]}'); explicit conversion accepts a single filesystem path only`)
136
+ }
137
+ // Already native or cwd-relative: an honest, DECLARED passthrough — the
138
+ // description promises this, so no mount facts are consulted at all.
139
+ if (isWindowsNativeValue(value)) {
140
+ return { ok: true, native_path: value, status: 'passthrough', kind: 'windows-native' }
141
+ }
142
+ if (!looksPosixValue(value)) {
143
+ return { ok: true, native_path: value, status: 'passthrough', kind: 'cwd-relative' }
144
+ }
145
+ if (typeof translate !== 'function') {
146
+ return reject('NO_TRANSLATOR', `no shared translator is wired; refusing to guess a native form for ${JSON.stringify(value)}`)
147
+ }
148
+ let translated
149
+ try {
150
+ translated = translate(value, env ?? null)
151
+ } catch (error) {
152
+ return reject('TRANSLATE_FAILED', `the shared translator failed for ${JSON.stringify(value)}: ${error?.message ?? error}`)
153
+ }
154
+ if (typeof translated !== 'string' || translated.length === 0) {
155
+ return reject('TRANSLATE_FAILED', `the shared translator returned a non-string for ${JSON.stringify(value)}`)
156
+ }
157
+ if (translated !== value) {
158
+ return { ok: true, native_path: translated, status: 'converted', kind: 'msys' }
159
+ }
160
+ return reject('NO_MOUNT_MAPPING', `no MSYS mount mapping for ${JSON.stringify(value)} (${factSummary(env)}). Refusing to return the POSIX spelling as a native path — resolve the mount fact or convert the value at its source.`)
161
+ }
162
+
163
+ /** The model-facing contract (kept in one place for both definition shapes). */
164
+ export function pathToolDescription() {
165
+ return 'Explicitly convert one complete caller-declared path value from the MSYS/Git Bash dialect '
166
+ + '(/c/-style drive roots, ~, $HOME-style variables, /tmp, /dev/null, Git installation mounts) '
167
+ + 'to its native Windows form and return { native_path }. Call this ONLY when the full value is '
168
+ + 'already known to be a single filesystem path — the tool never inspects surrounding data and never guesses. '
169
+ + 'Pure string conversion through the plugin\'s shared mount table: no filesystem I/O, no existence check, '
170
+ + 'no permission grant; your runtime policy still governs the converted path. Values that are already '
171
+ + 'Windows-native (drive-letter or UNC) or cwd-relative are returned unchanged, by design. A POSIX-looking '
172
+ + 'value that no mount maps — or whose needed mount fact is unavailable — is REJECTED with an explicit error '
173
+ + 'instead of a misleading passthrough. Independent of the automatic dialect switches; the parameter is named '
174
+ + 'msys_path so the automatic path-field rewrite leaves it untouched. '
175
+ + 'Use it for runtime-built MSYS paths before direct native Node fs/path I/O: '
176
+ + 'await the result and pass result.native_path, never the original MSYS string. '
177
+ + 'Already-native/cwd-relative Node values or ordinary translated tool path fields need no conversion. '
178
+ + 'Never pass routes, search strings, shell commands or program source. '
179
+ + 'In PTC invoke this offered SDK binding from inside run_code. A file policy/access denial is a stop condition, '
180
+ + 'not a reason to convert the spelling and retry through Node or another tool.'
181
+ }
182
+
183
+ const MSYS_PATH_PARAM_DESCRIPTION = 'The complete value to convert, as one single MSYS/Git Bash path '
184
+ + '(drive root /c/, ~, $HOME-style variable, /tmp, /dev/null or a Git install mount). '
185
+ + 'Pass the FULL value only; the tool performs no I/O and no permission grant.'
186
+
187
+ /**
188
+ * Build the tool definition in whichever dialect the host supports: the real
189
+ * `defineTool` author schema when the host module is available (parameters is
190
+ * a property map with `required: true` on the property; the output object
191
+ * declares `additionalProperties: false` explicitly and marks every property
192
+ * required), or an equivalent plain JSON-schema object otherwise. Both shapes
193
+ * share ONE execute body.
194
+ * @param {object} deps
195
+ * @param {object|undefined} [deps.toolsModule] - `@deepseek-ai/dsh-tools` (defineTool).
196
+ * @param {(value: string, env?: object|null) => unknown} [deps.translate] -
197
+ * the shared translator, already composed fresh by the wiring.
198
+ * @param {(value: string, env: object|null) => unknown} [deps.translateMsysPath] -
199
+ * alternative to `translate`: the raw shared helper.
200
+ * @param {() => object|null|undefined} [deps.buildEnv] - fresh mount-facts
201
+ * reader (buildTranslateEnv composition); called once per tool call.
202
+ * @param {(message: string) => void} [deps.log]
203
+ * @returns {object} a registry-ready definition.
204
+ */
205
+ export function buildPathToolDefinition({ toolsModule, translate, translateMsysPath, buildEnv, experimentalEnabled, log = () => {} } = {}) {
206
+ const convertValue = typeof translate === 'function'
207
+ ? (value, env) => translate(value, env)
208
+ : typeof translateMsysPath === 'function'
209
+ ? (value, env) => translateMsysPath(value, env)
210
+ : undefined
211
+ if (!convertValue) {
212
+ throw new Error('path-tool: a shared translator is required — pass translate (fresh composed) or translateMsysPath; the tool never copies the mount table')
213
+ }
214
+ const execute = (args) => {
215
+ if (experimentalEnabled?.() === false) throw new Error('gitbash_to_native_path is disabled by experimentalNative. Enable Experimental native execution / fail-closed in Settings > Plugins > dsh-gitbash-shell.')
216
+ // Fresh facts per call: the same lazy discipline as every other
217
+ // translation consumer (a settings bashPath change must be visible on
218
+ // the next call without a re-mount).
219
+ const env = typeof buildEnv === 'function' ? buildEnv() : undefined
220
+ const outcome = convertMsysToNative(args?.msys_path, { translate: convertValue, env })
221
+ if (!outcome.ok) throw new Error(outcome.message)
222
+ return { native_path: outcome.native_path }
223
+ }
224
+ if (typeof toolsModule?.defineTool === 'function') {
225
+ return toolsModule.defineTool({
226
+ name: PATH_TOOL_NAME,
227
+ description: pathToolDescription(),
228
+ parameters: {
229
+ msys_path: { type: 'string', required: true, description: MSYS_PATH_PARAM_DESCRIPTION },
230
+ },
231
+ output: {
232
+ schema: {
233
+ type: 'object',
234
+ properties: { native_path: { type: 'string', required: true } },
235
+ additionalProperties: false,
236
+ },
237
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
238
+ },
239
+ isConcurrencySafe: true,
240
+ execute,
241
+ })
242
+ }
243
+ log('path-tool: @deepseek-ai/dsh-tools not resolvable; building a plain JSON-schema definition')
244
+ return {
245
+ name: PATH_TOOL_NAME,
246
+ description: pathToolDescription(),
247
+ parameters: {
248
+ type: 'object',
249
+ properties: { msys_path: { type: 'string', description: MSYS_PATH_PARAM_DESCRIPTION } },
250
+ required: ['msys_path'],
251
+ additionalProperties: false,
252
+ },
253
+ output: {
254
+ schema: {
255
+ type: 'object',
256
+ properties: { native_path: { type: 'string' } },
257
+ required: ['native_path'],
258
+ additionalProperties: false,
259
+ },
260
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
261
+ },
262
+ execute,
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Build the cordis module the main wiring mounts inside an agent-owned scope.
268
+ * The tool registration is owned by the mounting scope: it reverts through
269
+ * the scope's effect at disposal, exactly like the bash-tool module's.
270
+ * Windows-only availability, agent gating and any dialect/config gate are the
271
+ * MAIN WIRING's decisions — this module carries none.
272
+ *
273
+ * Deps (final interface):
274
+ * - toolsModule: `@deepseek-ai/dsh-tools` when resolvable (defineTool);
275
+ * absent ⇒ plain JSON-schema definition, same surface.
276
+ * - translate: (value) => native, the shared translateMsysPath
277
+ * composed fresh per call by the wiring, OR
278
+ * - translateMsysPath + buildEnv: the raw shared helper plus a fresh
279
+ * mount-facts reader `() => buildTranslateEnv(...)`;
280
+ * buildEnv is called once per tool call.
281
+ * - log: optional (message) => void.
282
+ *
283
+ * @param {object} [deps]
284
+ * @returns {{ inject: string[], apply: (ctx: object) => void }}
285
+ */
286
+ export function createPathToolModule(deps = {}) {
287
+ const definition = buildPathToolDefinition(deps)
288
+ return {
289
+ inject: ['tools'],
290
+ apply(ctx) {
291
+ if (typeof ctx?.tools?.register !== 'function') {
292
+ throw new Error('path-tool: ctx.tools.register is unavailable; cannot own the tool registration')
293
+ }
294
+ registerExperimentalTool(ctx, () => definition, deps.experimentalEnabled, { scope: deps.scope, log: deps.log })
295
+ },
296
+ }
297
+ }
@@ -0,0 +1,12 @@
1
+ /** Use the host's current directory; retain the old policy/header seam when absent. */
2
+ export async function resolveSessionCwd(ctx, exec, policy) {
3
+ if (exec.agent === undefined) return undefined
4
+ exec.signal?.throwIfAborted()
5
+ const service = typeof ctx.get === 'function' ? ctx.get('workingDirectory') : undefined
6
+ if (service !== undefined) {
7
+ if (typeof service?.ensure !== 'function') throw new TypeError('workingDirectory service does not expose ensure()')
8
+ // Existing service errors must not become a silent downgrade to the original cwd.
9
+ return service.ensure(exec.agent, exec.signal)
10
+ }
11
+ return policy?.workspaceRoot ?? exec.agent.session?.header?.cwd
12
+ }