@namzu/sdk 50.1.0 → 51.1.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/CHANGELOG.md +28 -0
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/execution/code-runtime/types.d.ts +35 -0
- package/dist/execution/code-runtime/types.d.ts.map +1 -1
- package/dist/execution/code-runtime/types.js.map +1 -1
- package/dist/execution/code-runtime/worker-program.d.ts +1 -1
- package/dist/execution/code-runtime/worker-program.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker-program.js +72 -13
- package/dist/execution/code-runtime/worker-program.js.map +1 -1
- package/dist/execution/code-runtime/worker.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker.js +35 -1
- package/dist/execution/code-runtime/worker.js.map +1 -1
- package/dist/execution/workflow/errors.d.ts +5 -0
- package/dist/execution/workflow/errors.d.ts.map +1 -0
- package/dist/execution/workflow/errors.js +8 -0
- package/dist/execution/workflow/errors.js.map +1 -0
- package/dist/execution/workflow/index.d.ts +9 -0
- package/dist/execution/workflow/index.d.ts.map +1 -0
- package/dist/execution/workflow/index.js +6 -0
- package/dist/execution/workflow/index.js.map +1 -0
- package/dist/execution/workflow/journal.d.ts +18 -0
- package/dist/execution/workflow/journal.d.ts.map +1 -0
- package/dist/execution/workflow/journal.js +43 -0
- package/dist/execution/workflow/journal.js.map +1 -0
- package/dist/execution/workflow/meta.d.ts +14 -0
- package/dist/execution/workflow/meta.d.ts.map +1 -0
- package/dist/execution/workflow/meta.js +314 -0
- package/dist/execution/workflow/meta.js.map +1 -0
- package/dist/execution/workflow/phases.d.ts +32 -0
- package/dist/execution/workflow/phases.d.ts.map +1 -0
- package/dist/execution/workflow/phases.js +57 -0
- package/dist/execution/workflow/phases.js.map +1 -0
- package/dist/execution/workflow/prelude.d.ts +15 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -0
- package/dist/execution/workflow/prelude.js +128 -0
- package/dist/execution/workflow/prelude.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts +31 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -0
- package/dist/execution/workflow/runtime.js +682 -0
- package/dist/execution/workflow/runtime.js.map +1 -0
- package/dist/execution/workflow/schema.d.ts +10 -0
- package/dist/execution/workflow/schema.d.ts.map +1 -0
- package/dist/execution/workflow/schema.js +145 -0
- package/dist/execution/workflow/schema.js.map +1 -0
- package/dist/execution/workflow/types.d.ts +267 -0
- package/dist/execution/workflow/types.d.ts.map +1 -0
- package/dist/execution/workflow/types.js +7 -0
- package/dist/execution/workflow/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +2 -1
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/process/kill-tree.d.ts +7 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +41 -0
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/errors.d.ts +10 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +16 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/retry.d.ts +27 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +19 -3
- package/dist/provider/retry.js.map +1 -1
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +31 -2
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -1
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +1 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/job-id.d.ts +21 -0
- package/dist/runtime/jobs/job-id.d.ts.map +1 -0
- package/dist/runtime/jobs/job-id.js +66 -0
- package/dist/runtime/jobs/job-id.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +0 -1
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +2 -3
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +24 -1
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +5 -1
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +8 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +20 -3
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +4 -0
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +4 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts +55 -0
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +174 -13
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/provider/error.d.ts +6 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +15 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +5 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/utils/backoff.d.ts +12 -0
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +15 -0
- package/dist/utils/backoff.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/AGENTS.md +134 -46
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/execution/code-runtime/types.ts +37 -1
- package/src/execution/code-runtime/worker-program.ts +72 -13
- package/src/execution/code-runtime/worker.ts +40 -1
- package/src/execution/workflow/errors.ts +7 -0
- package/src/execution/workflow/index.ts +8 -0
- package/src/execution/workflow/journal.ts +46 -0
- package/src/execution/workflow/meta.ts +316 -0
- package/src/execution/workflow/phases.ts +91 -0
- package/src/execution/workflow/prelude.ts +135 -0
- package/src/execution/workflow/runtime.ts +786 -0
- package/src/execution/workflow/schema.ts +137 -0
- package/src/execution/workflow/types.ts +300 -0
- package/src/index.ts +4 -0
- package/src/plugin/shell-hook.ts +2 -1
- package/src/process/kill-tree.ts +43 -0
- package/src/provider/errors.ts +18 -0
- package/src/provider/retry.ts +53 -3
- package/src/provider/token-budget.ts +31 -1
- package/src/public-runtime.ts +11 -0
- package/src/public-tools.ts +6 -0
- package/src/public-types.ts +3 -0
- package/src/runtime/jobs/job-id.ts +66 -0
- package/src/runtime/jobs/registry.ts +2 -3
- package/src/runtime/query/iteration/stream-turn.ts +24 -1
- package/src/runtime/query/prompt.ts +5 -1
- package/src/tools/builtins/bash.ts +33 -3
- package/src/tools/builtins/job.ts +4 -0
- package/src/tools/builtins/wait-for-job.ts +4 -0
- package/src/tools/command-shell.ts +194 -11
- package/src/types/provider/error.ts +6 -0
- package/src/types/provider/errors.ts +16 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +5 -0
- package/src/utils/backoff.ts +21 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { estimateMessagesTokens } from '../compaction/token-estimate.js'
|
|
1
2
|
import { EMPTY_TOKEN_USAGE } from '../constants/limits.js'
|
|
2
3
|
import type { SessionTokenBudget } from '../store/budget/ledger.js'
|
|
3
4
|
import { type TokenUsage, mergeTokenUsage } from '../types/common/index.js'
|
|
4
5
|
import { ProviderError, classifyProviderError } from '../types/provider/errors.js'
|
|
5
6
|
import type { LLMProvider, StreamChunk } from '../types/provider/index.js'
|
|
7
|
+
import { cancelCauseOf } from '../types/session/cancel-cause.js'
|
|
6
8
|
import { isProviderRequestError } from './errors.js'
|
|
7
9
|
|
|
8
10
|
export class TokenBudgetAdmissionError extends ProviderError {
|
|
@@ -49,6 +51,7 @@ export function withTokenBudget(provider: LLMProvider, budget: SessionTokenBudge
|
|
|
49
51
|
let completed = false
|
|
50
52
|
let outputObserved = false
|
|
51
53
|
let uncertainRetry = false
|
|
54
|
+
let streamedChars = 0
|
|
52
55
|
let contacted = false
|
|
53
56
|
let abandoned = false
|
|
54
57
|
let iterator: AsyncIterator<StreamChunk> | undefined
|
|
@@ -107,6 +110,7 @@ export function withTokenBudget(provider: LLMProvider, budget: SessionTokenBudge
|
|
|
107
110
|
})
|
|
108
111
|
}
|
|
109
112
|
if (Object.keys(chunk.delta).length > 0) outputObserved = true
|
|
113
|
+
if (typeof chunk.delta.content === 'string') streamedChars += chunk.delta.content.length
|
|
110
114
|
if (chunk.error) throw new Error(chunk.error)
|
|
111
115
|
yield chunk
|
|
112
116
|
}
|
|
@@ -142,7 +146,33 @@ export function withTokenBudget(provider: LLMProvider, budget: SessionTokenBudge
|
|
|
142
146
|
} catch {
|
|
143
147
|
// Cleanup cannot replace the provider outcome.
|
|
144
148
|
}
|
|
145
|
-
if (!completed)
|
|
149
|
+
if (!completed) {
|
|
150
|
+
// A request the operator deliberately cancelled (a `user` cancel cause,
|
|
151
|
+
// not a parent or a runtime tearing the tree down) before any usage frame
|
|
152
|
+
// arrived is closed on an estimate (the prompt and what streamed), not
|
|
153
|
+
// left as unknown spend. Left open it poisons the whole tree: every
|
|
154
|
+
// sibling and every later child is refused for the rest of the turn,
|
|
155
|
+
// because of work somebody chose to stop. The estimate can under-count
|
|
156
|
+
// what the provider billed for the part it never reported; a failure
|
|
157
|
+
// that was not a cancellation keeps its marker as before.
|
|
158
|
+
if (
|
|
159
|
+
usage === undefined &&
|
|
160
|
+
signal?.aborted === true &&
|
|
161
|
+
cancelCauseOf(signal.reason) === 'user' &&
|
|
162
|
+
!uncertainRetry
|
|
163
|
+
) {
|
|
164
|
+
const prompt = estimateMessagesTokens(params.messages)
|
|
165
|
+
const completion = Math.ceil(streamedChars / 4)
|
|
166
|
+
await budget.finishRequest(requestId, {
|
|
167
|
+
...EMPTY_TOKEN_USAGE,
|
|
168
|
+
promptTokens: prompt,
|
|
169
|
+
completionTokens: completion,
|
|
170
|
+
totalTokens: prompt + completion,
|
|
171
|
+
})
|
|
172
|
+
} else {
|
|
173
|
+
await budget.failRequest(requestId, usage)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
146
176
|
}
|
|
147
177
|
}
|
|
148
178
|
|
package/src/public-runtime.ts
CHANGED
|
@@ -20,6 +20,17 @@ export { BackgroundJobOutputWaitLimitError } from './runtime/jobs/registry.js'
|
|
|
20
20
|
export { HostCallDeniedError } from './execution/code-runtime/types.js'
|
|
21
21
|
export { WorkerCodeRuntime } from './execution/code-runtime/worker.js'
|
|
22
22
|
|
|
23
|
+
// The workflow runtime: runs a model-authored orchestration script on the
|
|
24
|
+
// interpreter above, with `agent()` as its only host call.
|
|
25
|
+
export {
|
|
26
|
+
FileWorkflowJournal,
|
|
27
|
+
parseWorkflowMeta,
|
|
28
|
+
resolveWorkflowLimits,
|
|
29
|
+
summarizeWorkflowPhases,
|
|
30
|
+
WorkflowRuntime,
|
|
31
|
+
WorkflowScriptError,
|
|
32
|
+
} from './execution/workflow/index.js'
|
|
33
|
+
|
|
23
34
|
export {
|
|
24
35
|
CompactionConfigSchema,
|
|
25
36
|
PluginRuntimeConfigSchema,
|
package/src/public-tools.ts
CHANGED
|
@@ -54,9 +54,15 @@ export { builtinCommandArguments, commandArgumentOf } from './tools/command-argu
|
|
|
54
54
|
export {
|
|
55
55
|
type CommandShell,
|
|
56
56
|
type CommandShellProbe,
|
|
57
|
+
WINDOWS_BASH_GUIDANCE,
|
|
58
|
+
WINDOWS_CMD_FALLBACK_NOTICE,
|
|
57
59
|
findCommandShell,
|
|
58
60
|
findCommandShellForDialect,
|
|
61
|
+
findGitBash,
|
|
59
62
|
hostCommandShell,
|
|
63
|
+
isWindowsBash,
|
|
64
|
+
rewriteWindowsNullRedirect,
|
|
65
|
+
windowsShellNotice,
|
|
60
66
|
describeSpawnFailure,
|
|
61
67
|
hostShellSpawn,
|
|
62
68
|
installedCommandShellForDialect,
|
package/src/public-types.ts
CHANGED
|
@@ -32,6 +32,9 @@ export type { CoalesceOptions } from './streaming/coalesce.js'
|
|
|
32
32
|
export type * from './types/bidi/index.js'
|
|
33
33
|
export type * from './types/tool/index.js'
|
|
34
34
|
export type * from './types/job/index.js'
|
|
35
|
+
export type * from './execution/workflow/types.js'
|
|
36
|
+
export type { PhaseAgentView, PhaseRecord } from './execution/workflow/phases.js'
|
|
37
|
+
export type { WorkflowRuntimeOptions } from './execution/workflow/runtime.js'
|
|
35
38
|
export type { WorkerCodeRuntimeOptions } from './execution/code-runtime/worker.js'
|
|
36
39
|
export type {
|
|
37
40
|
CodeRunOutcome,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Background job ids: `b` and eight characters of `0-9a-z`.
|
|
5
|
+
*
|
|
6
|
+
* Random rather than counted. A counter restarts with every registry, so a
|
|
7
|
+
* resumed conversation that still says `job_1` about a dead job would be
|
|
8
|
+
* answered by whatever job happens to be `job_1` in the new session. A random
|
|
9
|
+
* id from a space of 36^8 does not collide across sessions in practice, and
|
|
10
|
+
* the registry additionally never hands out one it already holds.
|
|
11
|
+
*
|
|
12
|
+
* Ids are opaque to callers: nothing may parse them or rely on the shape.
|
|
13
|
+
*/
|
|
14
|
+
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
15
|
+
const ID_BODY_LENGTH = 8
|
|
16
|
+
const ID_PATTERN = /^b[0-9a-z]{8}$/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether `id` looks like one this SDK minted (`b` + 8 base36 characters).
|
|
20
|
+
* A helper only: the tools do not gate on it, because a host's own registry
|
|
21
|
+
* may use other ids. Ids are opaque; a host must not parse them.
|
|
22
|
+
*/
|
|
23
|
+
export function isBackgroundJobId(id: unknown): id is string {
|
|
24
|
+
return typeof id === 'string' && ID_PATTERN.test(id)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A fresh id. Bytes are mapped onto the alphabet by rejection so every
|
|
29
|
+
* character is uniform (256 is not a multiple of 36).
|
|
30
|
+
*/
|
|
31
|
+
export function createBackgroundJobId(isTaken: (id: string) => boolean = () => false): string {
|
|
32
|
+
for (;;) {
|
|
33
|
+
let body = ''
|
|
34
|
+
while (body.length < ID_BODY_LENGTH) {
|
|
35
|
+
for (const byte of randomBytes(ID_BODY_LENGTH * 2)) {
|
|
36
|
+
if (byte >= 252) continue // 252 = 36 * 7
|
|
37
|
+
body += ALPHABET[byte % 36]
|
|
38
|
+
if (body.length === ID_BODY_LENGTH) break
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const id = `b${body}`
|
|
42
|
+
if (!isTaken(id)) return id
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const MAX_JOB_ID_INPUT_LENGTH = 128
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The tools' only gate on an id a caller passes in. A host may supply its own
|
|
50
|
+
* registry with its own id format, so shape is NOT checked here: anything that
|
|
51
|
+
* is a sane single token goes on to the registry, whose "No background job
|
|
52
|
+
* <id>." answers for an unknown one. Returns a plain refusal for input that
|
|
53
|
+
* cannot be an id at all (not a string, empty, over 128 characters, or
|
|
54
|
+
* containing whitespace or control characters), otherwise undefined.
|
|
55
|
+
*/
|
|
56
|
+
export function invalidBackgroundJobIdInput(id: unknown): string | undefined {
|
|
57
|
+
if (typeof id !== 'string' || id.length === 0) return 'A background job id is required.'
|
|
58
|
+
if (id.length > MAX_JOB_ID_INPUT_LENGTH) {
|
|
59
|
+
return `That is not a background job id: it is longer than ${MAX_JOB_ID_INPUT_LENGTH} characters.`
|
|
60
|
+
}
|
|
61
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point.
|
|
62
|
+
if (/[\s\u0000-\u001f\u007f-\u009f]/u.test(id)) {
|
|
63
|
+
return 'That is not a background job id: ids contain no spaces or control characters.'
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
BackgroundJobOutputWaitResult,
|
|
11
11
|
} from '../../types/job/index.js'
|
|
12
12
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
13
|
+
import { createBackgroundJobId } from './job-id.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Work that outlives a tool call, owned by whoever started it.
|
|
@@ -213,7 +214,6 @@ const GROUP_POLL_MS = 250
|
|
|
213
214
|
|
|
214
215
|
export class BackgroundJobRegistry {
|
|
215
216
|
private readonly jobs = new Map<string, JobEntry>()
|
|
216
|
-
private counter = 0
|
|
217
217
|
private readonly exitListeners = new Set<(job: BackgroundJob) => void>()
|
|
218
218
|
private outputWaiters = 0
|
|
219
219
|
private readonly outputWaitersByOwner = new Map<string, number>()
|
|
@@ -303,8 +303,7 @@ export class BackgroundJobRegistry {
|
|
|
303
303
|
throw new BackgroundJobLimitError({ owner: params.owner, limit: this.maxJobs })
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
this.
|
|
307
|
-
const id = `job_${this.counter}`
|
|
306
|
+
const id = createBackgroundJobId((candidate) => this.jobs.has(candidate))
|
|
308
307
|
// Same scrub as the foreground path: inheritance is implicit, so the
|
|
309
308
|
// operator's provider credentials do not travel into a command nobody
|
|
310
309
|
// decided should see them. A background job outlives the call that
|
|
@@ -268,7 +268,7 @@ export async function* streamProviderTurn(
|
|
|
268
268
|
|
|
269
269
|
let id = ''
|
|
270
270
|
const model = ''
|
|
271
|
-
|
|
271
|
+
let text = new StreamTextAccumulator()
|
|
272
272
|
let finishReason: ChatCompletionResponse['finishReason'] = 'stop'
|
|
273
273
|
// What the stream itself reported, if anything. `finishReason` above
|
|
274
274
|
// defaults to 'stop' for the response; classifying unreadable tool input
|
|
@@ -413,8 +413,31 @@ export async function* streamProviderTurn(
|
|
|
413
413
|
code: chunk.retry.code,
|
|
414
414
|
...(chunk.retry.status !== undefined ? { status: chunk.retry.status } : {}),
|
|
415
415
|
serverDirected: chunk.retry.serverDirected,
|
|
416
|
+
...(chunk.retry.restart ? { restart: true } : {}),
|
|
416
417
|
})
|
|
417
418
|
yield* drainPending()
|
|
419
|
+
if (chunk.retry.restart) {
|
|
420
|
+
// The failed attempt's text and reasoning are void: the request
|
|
421
|
+
// starts over under the same message. The decorator never
|
|
422
|
+
// restarts once a tool call began, so no bucket is left behind.
|
|
423
|
+
text = new StreamTextAccumulator()
|
|
424
|
+
reasoningBuckets.clear()
|
|
425
|
+
citations.length = 0
|
|
426
|
+
replayState = undefined
|
|
427
|
+
id = ''
|
|
428
|
+
finishReason = 'stop'
|
|
429
|
+
reportedFinishReason = undefined
|
|
430
|
+
finishDetail = undefined
|
|
431
|
+
streamedLength = 0
|
|
432
|
+
lastOutputCall = undefined
|
|
433
|
+
usage = {
|
|
434
|
+
promptTokens: 0,
|
|
435
|
+
completionTokens: 0,
|
|
436
|
+
totalTokens: 0,
|
|
437
|
+
cachedTokens: 0,
|
|
438
|
+
cacheWriteTokens: 0,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
418
441
|
continue
|
|
419
442
|
}
|
|
420
443
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type PromptContributionRegistry,
|
|
6
6
|
SKILLS_CONTRIBUTION_ID,
|
|
7
7
|
} from '../../prompt/contributions.js'
|
|
8
|
+
import { isWindowsBash } from '../../tools/command-shell.js'
|
|
8
9
|
import type { ToolManager } from '../../toolsets/manager.js'
|
|
9
10
|
import type { AgentRuntimeContext } from '../../types/agent/base.js'
|
|
10
11
|
import type { AgentContextLevel } from '../../types/agent/factory.js'
|
|
@@ -135,8 +136,11 @@ function buildShellContext(config: PromptBuilderConfig): string | undefined {
|
|
|
135
136
|
)
|
|
136
137
|
continue
|
|
137
138
|
const dialect = tool.commandDialect({ sandboxed: config.sandboxed === true })
|
|
139
|
+
// Git Bash on a Windows host is named, so the line does not read as the
|
|
140
|
+
// Linux shell it is not. The syntax rules are in the tool's description.
|
|
141
|
+
const gitBash = !config.sandboxed && dialect === 'bash' && isWindowsBash()
|
|
138
142
|
lines.push(
|
|
139
|
-
`Execution shell for ${name}: ${dialect}${config.sandboxed ? ' (sandbox guest)' : ' (host)'}.`,
|
|
143
|
+
`Execution shell for ${name}: ${dialect}${config.sandboxed ? ' (sandbox guest)' : gitBash ? ' (Git Bash on Windows, host)' : ' (host)'}.`,
|
|
140
144
|
)
|
|
141
145
|
if (dialect === 'cmd')
|
|
142
146
|
lines.push(
|
|
@@ -8,8 +8,12 @@ import { subscribeToAbort } from '../../utils/abort.js'
|
|
|
8
8
|
import { readPositiveIntEnv } from '../../utils/env.js'
|
|
9
9
|
import {
|
|
10
10
|
type CommandShell,
|
|
11
|
+
WINDOWS_BASH_GUIDANCE,
|
|
11
12
|
bashToolDialect,
|
|
12
13
|
describeSpawnFailure,
|
|
14
|
+
hostCommandShell,
|
|
15
|
+
isWindowsBash,
|
|
16
|
+
rewriteWindowsNullRedirect,
|
|
13
17
|
sandboxShellSpawn,
|
|
14
18
|
spawnHostShell,
|
|
15
19
|
withoutBashStartup,
|
|
@@ -57,6 +61,12 @@ const inputSchema = z.object({
|
|
|
57
61
|
command: z
|
|
58
62
|
.string()
|
|
59
63
|
.min(1)
|
|
64
|
+
// Git Bash on Windows: a model that writes `>nul` meant the null device.
|
|
65
|
+
// Rewritten here, before the permission rules read the line and before
|
|
66
|
+
// it runs, so the two see the same text.
|
|
67
|
+
.transform((command) =>
|
|
68
|
+
isWindowsBash(hostCommandShell()) ? rewriteWindowsNullRedirect(command) : command,
|
|
69
|
+
)
|
|
60
70
|
.describe(
|
|
61
71
|
'The bash command to execute. Required, non-empty. Single command per call (use `&&` / `;` chaining for compound commands). Avoid heredocs that span more than a few hundred bytes — large content should be created with `write`, then extended with `edit` insertLine: "end", not piped into bash.',
|
|
62
72
|
),
|
|
@@ -336,10 +346,12 @@ export const SANDBOX_CANNOT_DETACH =
|
|
|
336
346
|
export const SANDBOX_ESCAPE_NOT_APPROVED =
|
|
337
347
|
'dangerously_disable_sandbox was not approved for this call, so the command did not run. Leaving the sandbox needs the user to confirm it each time, and this turn either cannot ask or does not allow it. Run the command inside the sandbox, or tell the user what it needs and let them run it.'
|
|
338
348
|
|
|
339
|
-
|
|
349
|
+
const BASH_DESCRIPTION =
|
|
350
|
+
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required — never call this tool with empty arguments. For very long content (e.g. building a large file), prefer `write` for the opening and `edit` with insertLine: "end" for follow-up chunks over a heredoc to avoid hitting the output token limit mid-stream.'
|
|
351
|
+
|
|
352
|
+
const BashToolBase = defineTool({
|
|
340
353
|
name: 'bash',
|
|
341
|
-
description:
|
|
342
|
-
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required — never call this tool with empty arguments. For very long content (e.g. building a large file), prefer `write` for the opening and `edit` with insertLine: "end" for follow-up chunks over a heredoc to avoid hitting the output token limit mid-stream.',
|
|
354
|
+
description: BASH_DESCRIPTION,
|
|
343
355
|
inputSchema,
|
|
344
356
|
// A command carries quotes and backslashes of its own, and a heredoc
|
|
345
357
|
// carries newlines: copied into the JSON string raw, they end it.
|
|
@@ -721,3 +733,21 @@ function describeWithheldEnv(dropped: readonly string[]): string {
|
|
|
721
733
|
const names = rest > 0 ? `${shown}, and ${rest} more` : shown
|
|
722
734
|
return `NOTE: ${dropped.length} credential-shaped environment variable(s) were withheld from this command and are unset rather than empty: ${names}. A host that means this command to have one passes it explicitly.`
|
|
723
735
|
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* The same tool, whose description gains the Windows guidance when the host
|
|
739
|
+
* shell is Git Bash. Resolved on first read, not at import: finding Git Bash
|
|
740
|
+
* runs `where.exe`, which a program that only imports the SDK must not pay.
|
|
741
|
+
*/
|
|
742
|
+
export const BashTool: typeof BashToolBase = Object.defineProperty(
|
|
743
|
+
{ ...BashToolBase },
|
|
744
|
+
'description',
|
|
745
|
+
{
|
|
746
|
+
enumerable: true,
|
|
747
|
+
configurable: true,
|
|
748
|
+
get: () =>
|
|
749
|
+
isWindowsBash(hostCommandShell())
|
|
750
|
+
? `${BASH_DESCRIPTION} ${WINDOWS_BASH_GUIDANCE}`
|
|
751
|
+
: BASH_DESCRIPTION,
|
|
752
|
+
},
|
|
753
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { invalidBackgroundJobIdInput } from '../../runtime/jobs/job-id.js'
|
|
3
4
|
import { defineTool } from '../defineTool.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -90,6 +91,9 @@ export const JobTool = defineTool({
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
const badId = invalidBackgroundJobIdInput(input.id)
|
|
95
|
+
if (badId) return { success: false, output: '', error: badId }
|
|
96
|
+
|
|
93
97
|
try {
|
|
94
98
|
if (input.action === 'kill') {
|
|
95
99
|
const killed = await jobs.kill(input.id)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { invalidBackgroundJobIdInput } from '../../runtime/jobs/job-id.js'
|
|
3
4
|
import { readPositiveIntEnv } from '../../utils/env.js'
|
|
4
5
|
import { defineTool } from '../defineTool.js'
|
|
5
6
|
import { describeJobWaitTimeout, waitForJobWithBounds } from './wait-for-job-bounds.js'
|
|
@@ -131,6 +132,9 @@ export const WaitForJobTool = defineTool({
|
|
|
131
132
|
}
|
|
132
133
|
const jobs = context.backgroundJobs
|
|
133
134
|
|
|
135
|
+
const badId = invalidBackgroundJobIdInput(input.id)
|
|
136
|
+
if (badId) return { success: false, output: '', error: badId }
|
|
137
|
+
|
|
134
138
|
try {
|
|
135
139
|
jobs.get(input.id)
|
|
136
140
|
} catch (err) {
|
|
@@ -32,12 +32,30 @@
|
|
|
32
32
|
* those would change what a command line means after the rules read it, so
|
|
33
33
|
* they are removed from the environment of the spawned bash.
|
|
34
34
|
* `NAMZU_BASH_SHELL=/bin/sh` restores the old shell exactly.
|
|
35
|
+
*
|
|
36
|
+
* ## Windows
|
|
37
|
+
*
|
|
38
|
+
* `cmd.exe` is a dialect the permission lexer cannot read: every command
|
|
39
|
+
* line comes back opaque, so every call needs a person in every mode, and a
|
|
40
|
+
* model writes POSIX syntax that cmd cannot run. So on Windows the same rule
|
|
41
|
+
* applies as everywhere else: run bash where it exists, and read the line as
|
|
42
|
+
* bash. The bash that exists on a Windows machine is Git for Windows'
|
|
43
|
+
* (`findGitBash`). Without it the tool falls back to cmd, as before, and
|
|
44
|
+
* {@link windowsShellNotice} says so once. `NAMZU_BASH_SHELL=cmd` keeps cmd
|
|
45
|
+
* on a machine that has Git Bash.
|
|
35
46
|
*/
|
|
36
47
|
|
|
37
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
type ChildProcess,
|
|
50
|
+
type SpawnOptions,
|
|
51
|
+
execFileSync,
|
|
52
|
+
spawn as nodeSpawn,
|
|
53
|
+
} from 'node:child_process'
|
|
54
|
+
import { randomUUID } from 'node:crypto'
|
|
38
55
|
import { constants, accessSync, existsSync } from 'node:fs'
|
|
39
|
-
import { delimiter, isAbsolute, join } from 'node:path'
|
|
56
|
+
import { delimiter, isAbsolute, join, win32 } from 'node:path'
|
|
40
57
|
|
|
58
|
+
import { MSYS_TAG_VARIABLE, registerMsysShell } from '../process/kill-tree.js'
|
|
41
59
|
import type { ShellDialect } from '../types/tool/index.js'
|
|
42
60
|
|
|
43
61
|
/** The shell a host-side command runs in. */
|
|
@@ -48,6 +66,8 @@ export interface CommandShell {
|
|
|
48
66
|
readonly dialect: ShellDialect
|
|
49
67
|
/** Where the choice came from, for diagnostics. */
|
|
50
68
|
readonly source: 'override' | 'bash' | 'sh' | 'platform'
|
|
69
|
+
/** True when this is a Windows executable (Git Bash), which needs Windows-only spawn details. */
|
|
70
|
+
readonly windows?: true
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
/** What resolution looks at. Injected by tests to simulate a host without bash. */
|
|
@@ -55,6 +75,13 @@ export interface CommandShellProbe {
|
|
|
55
75
|
readonly env: NodeJS.ProcessEnv
|
|
56
76
|
readonly platform: NodeJS.Platform
|
|
57
77
|
readonly isExecutable: (path: string) => boolean
|
|
78
|
+
/**
|
|
79
|
+
* Windows only: the paths `where.exe git` prints, one per entry, in order.
|
|
80
|
+
* Absent or throwing means git was not found on PATH.
|
|
81
|
+
*/
|
|
82
|
+
readonly whereGit?: () => readonly string[]
|
|
83
|
+
/** Windows only: the working directory, whose own programs are never trusted. Default: `process.cwd()`. */
|
|
84
|
+
readonly cwd?: string
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
const WELL_KNOWN_BASH = ['/bin/bash', '/usr/bin/bash']
|
|
@@ -63,6 +90,66 @@ const WELL_KNOWN_SH = ['/bin/sh', '/usr/bin/sh']
|
|
|
63
90
|
/** Environment variables that change what a bash command line means. */
|
|
64
91
|
const BASH_STARTUP_VARIABLES = new Set(['BASH_ENV', 'ENV', 'SHELLOPTS', 'BASHOPTS'])
|
|
65
92
|
|
|
93
|
+
/** A variable of a Windows environment, whose names are not case-sensitive. */
|
|
94
|
+
function windowsEnv(env: NodeJS.ProcessEnv, name: string): string | undefined {
|
|
95
|
+
const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase())
|
|
96
|
+
return key === undefined ? undefined : env[key]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Is `path` the working directory or inside it? Windows paths compare without case. */
|
|
100
|
+
function insideDirectory(path: string, directory: string): boolean {
|
|
101
|
+
const relative = win32.relative(directory, path)
|
|
102
|
+
return relative === '' || (!relative.startsWith('..') && !win32.isAbsolute(relative))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Git for Windows' bash.exe, found in this order:
|
|
107
|
+
*
|
|
108
|
+
* 1. `%ProgramFiles%\Git\bin\bash.exe`, then `%ProgramFiles(x86)%\Git\bin\bash.exe`;
|
|
109
|
+
* 2. `%LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe`, a per-user install;
|
|
110
|
+
* 3. the install that holds the first `git` that `where.exe git` lists: its
|
|
111
|
+
* `bin\bash.exe`, one or two directories up from the `git.exe` it printed.
|
|
112
|
+
* A hit inside the working directory is skipped, because a repository can
|
|
113
|
+
* carry a `git.exe` of its own and the search must not run it.
|
|
114
|
+
*
|
|
115
|
+
* There is no registry lookup, and `where bash` is never asked: on a machine
|
|
116
|
+
* with WSL, `System32\bash.exe` is the launcher of another system.
|
|
117
|
+
*/
|
|
118
|
+
export function findGitBash(probe: CommandShellProbe): string | undefined {
|
|
119
|
+
const programFiles = windowsEnv(probe.env, 'ProgramFiles') ?? 'C:\\Program Files'
|
|
120
|
+
const programFilesX86 = windowsEnv(probe.env, 'ProgramFiles(x86)') ?? 'C:\\Program Files (x86)'
|
|
121
|
+
for (const root of [programFiles, programFilesX86]) {
|
|
122
|
+
const candidate = win32.join(root, 'Git', 'bin', 'bash.exe')
|
|
123
|
+
if (probe.isExecutable(candidate)) return candidate
|
|
124
|
+
}
|
|
125
|
+
// A per-user install (the installer's "just me" choice) lives under LOCALAPPDATA.
|
|
126
|
+
const localAppData = windowsEnv(probe.env, 'LOCALAPPDATA')
|
|
127
|
+
if (localAppData !== undefined && localAppData !== '') {
|
|
128
|
+
const candidate = win32.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe')
|
|
129
|
+
if (probe.isExecutable(candidate)) return candidate
|
|
130
|
+
}
|
|
131
|
+
let listed: readonly string[] = []
|
|
132
|
+
try {
|
|
133
|
+
listed = probe.whereGit?.() ?? []
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
const cwd = probe.cwd ?? process.cwd()
|
|
138
|
+
for (const line of listed) {
|
|
139
|
+
const git = line.trim()
|
|
140
|
+
if (!win32.isAbsolute(git) || insideDirectory(git, cwd)) continue
|
|
141
|
+
const directory = win32.dirname(git)
|
|
142
|
+
// <Git>\cmd\git.exe and <Git>\bin\git.exe, then <Git>\mingw64\bin\git.exe.
|
|
143
|
+
for (const candidate of [
|
|
144
|
+
win32.join(directory, '..', 'bin', 'bash.exe'),
|
|
145
|
+
win32.join(directory, '..', '..', 'bin', 'bash.exe'),
|
|
146
|
+
]) {
|
|
147
|
+
if (probe.isExecutable(candidate)) return candidate
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
66
153
|
export function findCommandShell(probe: CommandShellProbe): CommandShell {
|
|
67
154
|
const override = probe.env.NAMZU_BASH_SHELL
|
|
68
155
|
if (override !== undefined && override !== '') {
|
|
@@ -76,11 +163,27 @@ export function findCommandShell(probe: CommandShellProbe): CommandShell {
|
|
|
76
163
|
?.toLowerCase()
|
|
77
164
|
.replace(/\.exe$/, '')
|
|
78
165
|
: override.slice(override.lastIndexOf('/') + 1)
|
|
79
|
-
|
|
166
|
+
// cmd is not a POSIX shell and takes no `-c`: naming it keeps Node's
|
|
167
|
+
// own cmd path, which is how a Windows machine with Git Bash runs the
|
|
168
|
+
// tool as it did before bash was preferred.
|
|
169
|
+
if (probe.platform === 'win32' && name === 'cmd')
|
|
170
|
+
return { path: undefined, dialect: 'cmd', source: 'override' }
|
|
171
|
+
return {
|
|
172
|
+
path: override,
|
|
173
|
+
dialect: name === 'bash' ? 'bash' : 'sh',
|
|
174
|
+
source: 'override',
|
|
175
|
+
...(probe.platform === 'win32' ? { windows: true as const } : {}),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Looking `bash` up on the Windows PATH can find WSL's launcher, which
|
|
179
|
+
// runs the command in another system, so only Git for Windows' own
|
|
180
|
+
// install is used. Without it Node's platform shell (cmd) runs the line.
|
|
181
|
+
if (probe.platform === 'win32') {
|
|
182
|
+
const gitBash = findGitBash(probe)
|
|
183
|
+
return gitBash === undefined
|
|
184
|
+
? { path: undefined, dialect: 'cmd', source: 'platform' }
|
|
185
|
+
: { path: gitBash, dialect: 'bash', source: 'bash', windows: true }
|
|
80
186
|
}
|
|
81
|
-
// Windows keeps Node's platform shell. Looking `bash` up on its PATH can
|
|
82
|
-
// find WSL's launcher, which runs the command in another system.
|
|
83
|
-
if (probe.platform === 'win32') return { path: undefined, dialect: 'cmd', source: 'platform' }
|
|
84
187
|
for (const directory of (probe.env.PATH ?? '').split(delimiter)) {
|
|
85
188
|
if (directory === '' || !directory.startsWith('/')) continue
|
|
86
189
|
const candidate = join(directory, 'bash')
|
|
@@ -92,6 +195,55 @@ export function findCommandShell(probe: CommandShellProbe): CommandShell {
|
|
|
92
195
|
return { path: '/bin/sh', dialect: 'sh', source: 'sh' }
|
|
93
196
|
}
|
|
94
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Said once, in the session, when a Windows machine has no Git Bash and the
|
|
200
|
+
* `bash` tool runs cmd.exe. Undefined everywhere else.
|
|
201
|
+
*/
|
|
202
|
+
export const WINDOWS_CMD_FALLBACK_NOTICE =
|
|
203
|
+
'Git for Windows not found — shell commands run in cmd.exe and each one asks for approval. Install Git for Windows for full support.'
|
|
204
|
+
|
|
205
|
+
export function windowsShellNotice(shell: CommandShell = hostCommandShell()): string | undefined {
|
|
206
|
+
return shell.source === 'platform' && shell.path === undefined && shell.dialect === 'cmd'
|
|
207
|
+
? WINDOWS_CMD_FALLBACK_NOTICE
|
|
208
|
+
: undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Is the host `bash` tool Git Bash on Windows? The model is then told so, and
|
|
213
|
+
* the Windows-only spawn details apply.
|
|
214
|
+
*/
|
|
215
|
+
export function isWindowsBash(shell: CommandShell = hostCommandShell()): boolean {
|
|
216
|
+
return shell.windows === true && shell.dialect === 'bash'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** What a model needs to know when the `bash` tool is Git Bash on Windows, appended to the tool's description. */
|
|
220
|
+
export const WINDOWS_BASH_GUIDANCE =
|
|
221
|
+
'This tool runs Git Bash (POSIX sh), not cmd.exe or PowerShell. Use Unix shell syntax: `/dev/null` not `NUL`, forward slashes, `$VAR` not `%VAR%` or `$env:VAR`. To run PowerShell, call `powershell -NoProfile -Command "…"`.'
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Rewrite a redirect to Windows' `NUL` device (`>nul`, `2>NUL`, `&>nul`) to
|
|
225
|
+
* `/dev/null`. In bash `nul` is an ordinary file name, so the line would
|
|
226
|
+
* create a file called `nul` that Windows then cannot delete. A line with
|
|
227
|
+
* `<`, `$` or a backtick is left alone: it may be a heredoc or a string that
|
|
228
|
+
* only mentions the text.
|
|
229
|
+
*/
|
|
230
|
+
export function rewriteWindowsNullRedirect(command: string): string {
|
|
231
|
+
if (/[<$`]/.test(command)) return command
|
|
232
|
+
return command.replace(/(\d?&?>+[ \t]*)nul(?=\s|$|[|&;)\n])/gi, '$1/dev/null')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** `where.exe git`, run from the system directory by its absolute path, so PATH cannot stand in for it. */
|
|
236
|
+
function whereGit(): readonly string[] {
|
|
237
|
+
const root = process.env.SystemRoot ?? process.env.windir ?? 'C:\\Windows'
|
|
238
|
+
const output = execFileSync(win32.join(root, 'System32', 'where.exe'), ['git'], {
|
|
239
|
+
encoding: 'utf8',
|
|
240
|
+
windowsHide: true,
|
|
241
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
242
|
+
timeout: 5000,
|
|
243
|
+
})
|
|
244
|
+
return output.split(/\r?\n/)
|
|
245
|
+
}
|
|
246
|
+
|
|
95
247
|
/**
|
|
96
248
|
* An explicitly requested interpreter for a host script. Unlike the live
|
|
97
249
|
* `bash` tool's automatic choice, this never substitutes bash for sh (or sh
|
|
@@ -144,7 +296,12 @@ let resolved: CommandShell | undefined
|
|
|
144
296
|
*/
|
|
145
297
|
export function hostCommandShell(): CommandShell {
|
|
146
298
|
if (resolved === undefined) {
|
|
147
|
-
resolved = findCommandShell({
|
|
299
|
+
resolved = findCommandShell({
|
|
300
|
+
env: process.env,
|
|
301
|
+
platform: process.platform,
|
|
302
|
+
isExecutable,
|
|
303
|
+
whereGit,
|
|
304
|
+
})
|
|
148
305
|
}
|
|
149
306
|
return resolved
|
|
150
307
|
}
|
|
@@ -180,7 +337,17 @@ export function hostShellSpawn(
|
|
|
180
337
|
} {
|
|
181
338
|
if (shell.path === undefined) return { file: undefined, args: [command], env }
|
|
182
339
|
if (shell.dialect !== 'bash') return { file: shell.path, args: ['-c', command], env }
|
|
183
|
-
|
|
340
|
+
const clean = withoutBashStartup(env)
|
|
341
|
+
// Git for Windows: the line is read as bash, so `>nul` must already have
|
|
342
|
+
// become `/dev/null` (the tool's input does that before the rules read it;
|
|
343
|
+
// this covers a caller that did not), and git must never open an editor.
|
|
344
|
+
if (shell.windows === true)
|
|
345
|
+
return {
|
|
346
|
+
file: shell.path,
|
|
347
|
+
args: ['-c', rewriteWindowsNullRedirect(command)],
|
|
348
|
+
env: { ...clean, GIT_EDITOR: 'true' },
|
|
349
|
+
}
|
|
350
|
+
return { file: shell.path, args: ['-c', command], env: clean }
|
|
184
351
|
}
|
|
185
352
|
|
|
186
353
|
/** Why a command could not be started, in the words a model or operator reads. */
|
|
@@ -283,15 +450,31 @@ export function spawnHostShell(
|
|
|
283
450
|
},
|
|
284
451
|
): ChildProcess {
|
|
285
452
|
const spawnFn = options.spawnImpl ?? nodeSpawn
|
|
286
|
-
const
|
|
453
|
+
const shell = options.shell ?? hostCommandShell()
|
|
454
|
+
const spawned = hostShellSpawn(command, options.env, shell)
|
|
455
|
+
// Git Bash: tag the shell so a kill can find what a forked command left.
|
|
456
|
+
const tag = shell.windows === true && shell.path !== undefined ? randomUUID() : undefined
|
|
457
|
+
const resolved =
|
|
458
|
+
tag === undefined ? spawned : { ...spawned, env: { ...spawned.env, [MSYS_TAG_VARIABLE]: tag } }
|
|
287
459
|
const common: SpawnOptions = {
|
|
288
460
|
cwd: options.cwd,
|
|
289
461
|
env: resolved.env,
|
|
290
462
|
detached: options.detached ?? (options.platform ?? process.platform) !== 'win32',
|
|
291
463
|
windowsHide: true,
|
|
292
|
-
|
|
464
|
+
// Git Bash reads no terminal: a command that asks for input gets
|
|
465
|
+
// end-of-file (the null device) instead of waiting for a timeout.
|
|
466
|
+
...(options.stdio === undefined
|
|
467
|
+
? resolved.file !== undefined && shell.windows === true
|
|
468
|
+
? { stdio: ['ignore', 'pipe', 'pipe'] as SpawnOptions['stdio'] }
|
|
469
|
+
: {}
|
|
470
|
+
: { stdio: options.stdio }),
|
|
471
|
+
}
|
|
472
|
+
if (resolved.file !== undefined) {
|
|
473
|
+
const child = spawnFn(resolved.file, [...resolved.args], common)
|
|
474
|
+
if (tag !== undefined && child !== null && typeof child === 'object')
|
|
475
|
+
registerMsysShell(child, { bash: resolved.file, tag })
|
|
476
|
+
return child
|
|
293
477
|
}
|
|
294
|
-
if (resolved.file !== undefined) return spawnFn(resolved.file, [...resolved.args], common)
|
|
295
478
|
const windows = windowsCmdSpawn(command, resolved.env)
|
|
296
479
|
return spawnFn(windows.file, [...windows.args], {
|
|
297
480
|
...common,
|
|
@@ -45,4 +45,10 @@ export interface ProviderRequestErrorInit extends ProviderErrorInfo {
|
|
|
45
45
|
* still welcome; what is not welcome is a raw body passed straight in.
|
|
46
46
|
*/
|
|
47
47
|
readonly detail?: string
|
|
48
|
+
/**
|
|
49
|
+
* The provider's own `x-should-retry` verdict, when it sent one. Only `false`
|
|
50
|
+
* is acted on: a provider that says the same request must not be sent again
|
|
51
|
+
* is believed over the status code, so the retry loop leaves it alone.
|
|
52
|
+
*/
|
|
53
|
+
readonly shouldRetry?: boolean
|
|
48
54
|
}
|