@namzu/sdk 32.0.0 → 33.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 +157 -0
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +13 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +3 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +71 -16
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +3 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/http-redirect.d.ts +17 -0
- package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
- package/dist/connector/mcp/http-redirect.js +31 -0
- package/dist/connector/mcp/http-redirect.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +5 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/image-admission.d.ts +16 -0
- package/dist/connector/mcp/image-admission.d.ts.map +1 -0
- package/dist/connector/mcp/image-admission.js +288 -0
- package/dist/connector/mcp/image-admission.js.map +1 -0
- package/dist/connector/mcp/stdio.d.ts +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +66 -23
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +3 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/constants/tools/index.js +1 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/execution/base.d.ts +13 -0
- package/dist/execution/base.d.ts.map +1 -1
- package/dist/execution/base.js +103 -8
- package/dist/execution/base.js.map +1 -1
- package/dist/execution/errors.d.ts +12 -0
- package/dist/execution/errors.d.ts.map +1 -0
- package/dist/execution/errors.js +21 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/factory.d.ts.map +1 -1
- package/dist/execution/factory.js +1 -0
- package/dist/execution/factory.js.map +1 -1
- package/dist/execution/hybrid.d.ts +5 -0
- package/dist/execution/hybrid.d.ts.map +1 -1
- package/dist/execution/hybrid.js +36 -16
- package/dist/execution/hybrid.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/local.d.ts +13 -0
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +145 -19
- package/dist/execution/local.js.map +1 -1
- package/dist/execution/output.d.ts +27 -0
- package/dist/execution/output.d.ts.map +1 -0
- package/dist/execution/output.js +105 -0
- package/dist/execution/output.js.map +1 -0
- package/dist/execution/remote.d.ts +26 -2
- package/dist/execution/remote.d.ts.map +1 -1
- package/dist/execution/remote.js +86 -15
- package/dist/execution/remote.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/process/kill-tree.d.ts +11 -13
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +11 -13
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/capabilities.d.ts +2 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +6 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +31 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +7 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +201 -54
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +11 -0
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +4 -4
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +6 -4
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/bidi/index.d.ts +1 -1
- package/dist/runtime/bidi/index.d.ts.map +1 -1
- package/dist/runtime/bidi/index.js +1 -1
- package/dist/runtime/bidi/index.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +19 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +150 -26
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +43 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +626 -161
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +3 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +48 -4
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +10 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +67 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +33 -13
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +25 -22
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +5 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +66 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +45 -16
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +6 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +193 -41
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/delegating.d.ts +2 -1
- package/dist/scheduler/delegating.d.ts.map +1 -1
- package/dist/scheduler/delegating.js +6 -5
- package/dist/scheduler/delegating.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +2 -2
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +12 -4
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +17 -3
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +153 -34
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +33 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +76 -2
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +9 -11
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +2 -1
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +250 -6
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +100 -30
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/types/agent/manager.d.ts +1 -1
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +10 -1
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +12 -12
- package/dist/types/bidi/index.d.ts +19 -2
- package/dist/types/bidi/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +18 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/connector/execution-context.d.ts +3 -1
- package/dist/types/connector/execution-context.d.ts.map +1 -1
- package/dist/types/errors/catalog.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +13 -6
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/errors/index.d.ts.map +1 -1
- package/dist/types/errors/index.js +21 -8
- package/dist/types/errors/index.js.map +1 -1
- package/dist/types/execution/index.d.ts +43 -1
- package/dist/types/execution/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +14 -1
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/config.d.ts +9 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +17 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -10
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +77 -6
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +6 -2
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/dist/types/workspace/ref.d.ts +5 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/abort.d.ts +8 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +16 -0
- package/dist/utils/abort.js.map +1 -1
- package/package.json +4 -4
- package/src/authorization/gate.ts +1 -1
- package/src/bridge/a2a/mapper.ts +23 -4
- package/src/bridge/sse/mapper.ts +3 -0
- package/src/connector/index.ts +2 -0
- package/src/connector/mcp/adapter.ts +82 -21
- package/src/connector/mcp/client.ts +2 -1
- package/src/connector/mcp/http-redirect.ts +32 -0
- package/src/connector/mcp/http-sse.ts +5 -0
- package/src/connector/mcp/image-admission.ts +272 -0
- package/src/connector/mcp/stdio.ts +63 -21
- package/src/connector/mcp/streamable-http.ts +3 -0
- package/src/constants/tools/index.ts +1 -1
- package/src/execution/base.ts +124 -7
- package/src/execution/errors.ts +24 -0
- package/src/execution/factory.ts +1 -0
- package/src/execution/hybrid.ts +47 -16
- package/src/execution/index.ts +1 -0
- package/src/execution/local.ts +164 -19
- package/src/execution/output.ts +104 -0
- package/src/execution/remote.ts +122 -17
- package/src/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- package/src/provider/capabilities.ts +10 -0
- package/src/provider/fallback.ts +39 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/retry.ts +8 -0
- package/src/public-runtime.ts +7 -1
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- package/src/run/workspace-fingerprint.ts +5 -4
- package/src/runtime/bidi/index.ts +1 -1
- package/src/runtime/bidi/session.ts +177 -28
- package/src/runtime/query/executor.ts +769 -163
- package/src/runtime/query/index.ts +58 -7
- package/src/runtime/query/iteration/index.ts +72 -0
- package/src/runtime/query/iteration/phases/context.ts +5 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/request-rich-content.ts +39 -14
- package/src/runtime/query/result.ts +25 -23
- package/src/runtime/query/resume-pending.ts +83 -1
- package/src/runtime/query/tool-output-budget.ts +52 -17
- package/src/runtime/query/tooling.ts +10 -0
- package/src/sandbox/provider/local.ts +213 -45
- package/src/scheduler/delegating.ts +6 -5
- package/src/scheduler/local.ts +3 -2
- package/src/session/workspace/driver.ts +12 -4
- package/src/session/workspace/git-worktree.ts +173 -34
- package/src/skills/registry.ts +47 -0
- package/src/tools/builtins/computer-use.ts +86 -2
- package/src/tools/builtins/run-code.ts +9 -11
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- package/src/types/agent/manager.ts +1 -1
- package/src/types/agent/scheduler.ts +10 -1
- package/src/types/bidi/index.ts +19 -2
- package/src/types/computer-use/index.ts +19 -0
- package/src/types/connector/execution-context.ts +3 -0
- package/src/types/errors/catalog.ts +14 -6
- package/src/types/errors/index.ts +22 -8
- package/src/types/execution/index.ts +46 -1
- package/src/types/hitl/index.ts +11 -0
- package/src/types/message/index.ts +3 -2
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/config.ts +9 -0
- package/src/types/provider/interface.ts +18 -0
- package/src/types/run/events.ts +17 -3
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- package/src/types/tool/presentation.ts +10 -3
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
package/src/execution/local.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
|
+
import { killTree } from '../process/kill-tree.js'
|
|
4
5
|
import type { LocalExecutionContextConfig } from '../types/connector/index.js'
|
|
5
6
|
import type {
|
|
6
7
|
CommandExecutor,
|
|
@@ -9,8 +10,22 @@ import type {
|
|
|
9
10
|
ExecutionCapability,
|
|
10
11
|
ExecutionEnvironment,
|
|
11
12
|
} from '../types/execution/index.js'
|
|
13
|
+
import { subscribeToAbort } from '../utils/abort.js'
|
|
12
14
|
import type { Logger } from '../utils/logger.js'
|
|
13
15
|
import { BaseExecutionContext } from './base.js'
|
|
16
|
+
import { BoundedTailCapture } from './output.js'
|
|
17
|
+
|
|
18
|
+
const LOCAL_COMMAND_DEFAULT_TIMEOUT_MS = 30_000
|
|
19
|
+
const LOCAL_COMMAND_KILL_GRACE_MS = 3_000
|
|
20
|
+
const LOCAL_COMMAND_DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
|
|
21
|
+
const LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES = 64 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
type LocalCommandCancellation = 'caller' | 'deadline' | 'teardown'
|
|
24
|
+
|
|
25
|
+
interface ActiveLocalCommand {
|
|
26
|
+
readonly closed: Promise<void>
|
|
27
|
+
cancel(origin: LocalCommandCancellation): void
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
export interface LocalExecutionContextOptions {
|
|
16
31
|
id: string
|
|
@@ -19,6 +34,8 @@ export interface LocalExecutionContextOptions {
|
|
|
19
34
|
envVars?: Record<string, string>
|
|
20
35
|
capabilities?: ExecutionCapability[]
|
|
21
36
|
shell?: string
|
|
37
|
+
/** Retained bytes per stdout/stderr stream. Defaults to 4 MiB; maximum 64 MiB. */
|
|
38
|
+
maxOutputBytes?: number
|
|
22
39
|
log?: Logger
|
|
23
40
|
}
|
|
24
41
|
|
|
@@ -31,6 +48,14 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
|
|
|
31
48
|
private envVars: Record<string, string>
|
|
32
49
|
private capabilities: ExecutionCapability[]
|
|
33
50
|
private shell: string | undefined
|
|
51
|
+
private maxOutputBytes: number
|
|
52
|
+
/**
|
|
53
|
+
* Constructor-created contexts are intentionally executable before
|
|
54
|
+
* `initialize()`: `createCommandGate()` has always used that public path.
|
|
55
|
+
* Teardown closes admission synchronously; an explicit initialize reopens it.
|
|
56
|
+
*/
|
|
57
|
+
private acceptsCommands = true
|
|
58
|
+
private readonly activeCommands = new Set<ActiveLocalCommand>()
|
|
34
59
|
|
|
35
60
|
constructor(options: LocalExecutionContextOptions) {
|
|
36
61
|
super(options.log)
|
|
@@ -40,6 +65,17 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
|
|
|
40
65
|
this.envVars = options.envVars ?? {}
|
|
41
66
|
this.capabilities = options.capabilities ?? ['filesystem', 'process', 'shell']
|
|
42
67
|
this.shell = options.shell
|
|
68
|
+
const maxOutputBytes = options.maxOutputBytes ?? LOCAL_COMMAND_DEFAULT_MAX_OUTPUT_BYTES
|
|
69
|
+
if (
|
|
70
|
+
!Number.isSafeInteger(maxOutputBytes) ||
|
|
71
|
+
maxOutputBytes <= 0 ||
|
|
72
|
+
maxOutputBytes > LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES
|
|
73
|
+
) {
|
|
74
|
+
throw new RangeError(
|
|
75
|
+
`The value of "maxOutputBytes" is out of range. It must be a positive safe integer no greater than ${LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES}. Received ${maxOutputBytes}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
this.maxOutputBytes = maxOutputBytes
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
protected async doInitialize(): Promise<void> {
|
|
@@ -49,7 +85,23 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
|
|
|
49
85
|
this.log.info('Local context initialized', { 'namzu.execution.cwd': this.cwd })
|
|
50
86
|
}
|
|
51
87
|
|
|
52
|
-
protected
|
|
88
|
+
protected override onInitializationStarted(): void {
|
|
89
|
+
this.acceptsCommands = false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected override onInitializationCommitted(): void {
|
|
93
|
+
this.acceptsCommands = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected override onTeardownRequested(): void {
|
|
97
|
+
this.acceptsCommands = false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected async doTeardown(): Promise<void> {
|
|
101
|
+
const admitted = [...this.activeCommands]
|
|
102
|
+
for (const operation of admitted) operation.cancel('teardown')
|
|
103
|
+
await Promise.all(admitted.map((operation) => operation.closed))
|
|
104
|
+
}
|
|
53
105
|
|
|
54
106
|
getCwd(): string {
|
|
55
107
|
return this.cwd
|
|
@@ -92,63 +144,155 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
|
|
|
92
144
|
args: string[] = [],
|
|
93
145
|
options?: CommandOptions,
|
|
94
146
|
): Promise<CommandResult> {
|
|
147
|
+
if (!this.acceptsCommands) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Local execution context "${this.id}" is tearing down or torn down. Call initialize() before executing another command.`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
95
152
|
if (!this.hasCapability('process') && !this.hasCapability('shell')) {
|
|
96
153
|
return {
|
|
97
154
|
exitCode: 1,
|
|
98
155
|
stdout: '',
|
|
99
156
|
stderr: 'Command execution not available: context lacks process/shell capability',
|
|
157
|
+
stdoutTruncated: false,
|
|
158
|
+
stderrTruncated: false,
|
|
100
159
|
durationMs: 0,
|
|
101
160
|
}
|
|
102
161
|
}
|
|
103
162
|
|
|
104
163
|
const cwd = options?.cwd ? resolve(this.cwd, options.cwd) : this.cwd
|
|
105
164
|
const env = { ...process.env, ...this.envVars, ...options?.env }
|
|
106
|
-
const timeoutMs = options?.timeoutMs ??
|
|
165
|
+
const timeoutMs = options?.timeoutMs ?? LOCAL_COMMAND_DEFAULT_TIMEOUT_MS
|
|
166
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0) {
|
|
167
|
+
throw new RangeError(
|
|
168
|
+
`The value of "timeoutMs" is out of range. It must be an unsigned integer. Received ${timeoutMs}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
107
171
|
// node's spawn() only keeps argv separation when it execs the binary directly;
|
|
108
172
|
// with shell enabled it re-joins command+args into one unescaped string and hands
|
|
109
173
|
// it to sh/cmd.exe, so any caller-supplied arg becomes shell syntax rather than a
|
|
110
174
|
// literal value. Since `command`/`args` here are whatever a tool call passed in,
|
|
111
175
|
// shell interpretation must be an explicit opt-in, never the silent default.
|
|
112
176
|
const shell = options?.shell ?? this.shell ?? false
|
|
177
|
+
const callerSignal = options?.signal
|
|
178
|
+
if (callerSignal?.aborted) {
|
|
179
|
+
return {
|
|
180
|
+
exitCode: null,
|
|
181
|
+
stdout: '',
|
|
182
|
+
stderr: '',
|
|
183
|
+
stdoutTruncated: false,
|
|
184
|
+
stderrTruncated: false,
|
|
185
|
+
durationMs: 0,
|
|
186
|
+
termination: { origin: 'caller', admitted: false },
|
|
187
|
+
}
|
|
188
|
+
}
|
|
113
189
|
|
|
114
|
-
|
|
190
|
+
let operation: ActiveLocalCommand
|
|
191
|
+
const result = new Promise<CommandResult>((resolvePromise) => {
|
|
115
192
|
const start = performance.now()
|
|
116
193
|
const proc = spawn(command, args, {
|
|
117
194
|
cwd,
|
|
118
195
|
env,
|
|
119
196
|
shell,
|
|
120
|
-
|
|
197
|
+
// POSIX process-group ownership is the strongest boundary available
|
|
198
|
+
// to this host execution context. Windows uses taskkill /T in
|
|
199
|
+
// `killTree`; neither path relies on Node's direct-child timeout.
|
|
200
|
+
detached: process.platform !== 'win32',
|
|
121
201
|
})
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
|
|
203
|
+
const stdout = new BoundedTailCapture(this.maxOutputBytes)
|
|
204
|
+
const stderr = new BoundedTailCapture(this.maxOutputBytes)
|
|
205
|
+
let spawnError: Error | undefined
|
|
206
|
+
let cancellation: LocalCommandCancellation | undefined
|
|
207
|
+
let escalation: ReturnType<typeof setTimeout> | undefined
|
|
208
|
+
let deadline: ReturnType<typeof setTimeout> | undefined
|
|
209
|
+
let disposeCallerAbort: (() => void) | undefined
|
|
210
|
+
let settled = false
|
|
211
|
+
|
|
212
|
+
const terminate = (signal: NodeJS.Signals): void => {
|
|
213
|
+
killTree(proc, signal)
|
|
214
|
+
}
|
|
215
|
+
const cancel = (origin: LocalCommandCancellation): void => {
|
|
216
|
+
if (settled || cancellation !== undefined) return
|
|
217
|
+
cancellation = origin
|
|
218
|
+
terminate('SIGTERM')
|
|
219
|
+
escalation = setTimeout(() => {
|
|
220
|
+
escalation = undefined
|
|
221
|
+
terminate('SIGKILL')
|
|
222
|
+
}, LOCAL_COMMAND_KILL_GRACE_MS)
|
|
223
|
+
escalation.unref?.()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let resolveClosed: (() => void) | undefined
|
|
227
|
+
const closed = new Promise<void>((resolveClose) => {
|
|
228
|
+
resolveClosed = resolveClose
|
|
229
|
+
})
|
|
230
|
+
operation = { cancel, closed }
|
|
231
|
+
this.activeCommands.add(operation)
|
|
232
|
+
|
|
233
|
+
if (timeoutMs > 0) {
|
|
234
|
+
deadline = setTimeout(() => cancel('deadline'), timeoutMs)
|
|
235
|
+
deadline.unref?.()
|
|
236
|
+
}
|
|
125
237
|
|
|
126
238
|
proc.stdout?.on('data', (data: Buffer) => {
|
|
127
|
-
stdout
|
|
239
|
+
stdout.push(data)
|
|
128
240
|
})
|
|
129
241
|
|
|
130
242
|
proc.stderr?.on('data', (data: Buffer) => {
|
|
131
|
-
stderr
|
|
243
|
+
stderr.push(data)
|
|
132
244
|
})
|
|
133
245
|
|
|
134
|
-
proc.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
durationMs: Math.round(performance.now() - start),
|
|
140
|
-
})
|
|
246
|
+
proc.once('error', (err) => {
|
|
247
|
+
// `close` follows `error` and is the point at which inherited stdio
|
|
248
|
+
// has drained. Releasing ownership here would strand a descendant
|
|
249
|
+
// which still holds those descriptors.
|
|
250
|
+
spawnError = err
|
|
141
251
|
})
|
|
142
252
|
|
|
143
|
-
proc.
|
|
253
|
+
proc.once('close', (code, signal) => {
|
|
254
|
+
if (settled) return
|
|
255
|
+
settled = true
|
|
256
|
+
disposeCallerAbort?.()
|
|
257
|
+
if (deadline !== undefined) clearTimeout(deadline)
|
|
258
|
+
if (escalation !== undefined) {
|
|
259
|
+
clearTimeout(escalation)
|
|
260
|
+
escalation = undefined
|
|
261
|
+
// Do not retain a numeric process-group id for a later timer after
|
|
262
|
+
// close: the host can reuse it. Force the cancellation boundary now.
|
|
263
|
+
terminate('SIGKILL')
|
|
264
|
+
}
|
|
265
|
+
this.activeCommands.delete(operation)
|
|
266
|
+
resolveClosed?.()
|
|
267
|
+
if (spawnError !== undefined) {
|
|
268
|
+
if (stderr.retainedBytes > 0) stderr.push(Buffer.from('\n'))
|
|
269
|
+
stderr.push(Buffer.from(spawnError.message))
|
|
270
|
+
}
|
|
144
271
|
resolvePromise({
|
|
145
|
-
exitCode: 1,
|
|
146
|
-
stdout,
|
|
147
|
-
stderr:
|
|
272
|
+
exitCode: spawnError === undefined ? code : 1,
|
|
273
|
+
stdout: stdout.text,
|
|
274
|
+
stderr: stderr.text,
|
|
275
|
+
stdoutTruncated: stdout.truncated,
|
|
276
|
+
stderrTruncated: stderr.truncated,
|
|
148
277
|
durationMs: Math.round(performance.now() - start),
|
|
278
|
+
...(cancellation === undefined
|
|
279
|
+
? {}
|
|
280
|
+
: {
|
|
281
|
+
termination: {
|
|
282
|
+
origin: cancellation === 'deadline' ? ('timeout' as const) : cancellation,
|
|
283
|
+
admitted: true as const,
|
|
284
|
+
...(signal === null ? {} : { signal }),
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
149
287
|
})
|
|
150
288
|
})
|
|
289
|
+
|
|
290
|
+
if (callerSignal !== undefined) {
|
|
291
|
+
disposeCallerAbort = subscribeToAbort(callerSignal, () => cancel('caller'))
|
|
292
|
+
if (callerSignal.aborted) cancel('caller')
|
|
293
|
+
}
|
|
151
294
|
})
|
|
295
|
+
return result
|
|
152
296
|
}
|
|
153
297
|
|
|
154
298
|
toConfig(): LocalExecutionContextConfig {
|
|
@@ -160,6 +304,7 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
|
|
|
160
304
|
envVars: this.envVars,
|
|
161
305
|
capabilities: this.capabilities,
|
|
162
306
|
shell: this.shell,
|
|
307
|
+
maxOutputBytes: this.maxOutputBytes,
|
|
163
308
|
}
|
|
164
309
|
}
|
|
165
310
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retain the newest bytes of one command-output stream under a hard bound.
|
|
3
|
+
*
|
|
4
|
+
* Bytes stay undecoded until the command closes, so a UTF-8 code point split
|
|
5
|
+
* across ordinary stream chunks is not corrupted. The backing store grows
|
|
6
|
+
* geometrically up to the configured cap and becomes a ring only when output
|
|
7
|
+
* crosses it; no incoming chunk is retained by reference.
|
|
8
|
+
*/
|
|
9
|
+
export class BoundedTailCapture {
|
|
10
|
+
private storage = Buffer.alloc(0)
|
|
11
|
+
private start = 0
|
|
12
|
+
private length = 0
|
|
13
|
+
private lostBytes = false
|
|
14
|
+
|
|
15
|
+
constructor(private readonly capBytes: number) {
|
|
16
|
+
if (!Number.isSafeInteger(capBytes) || capBytes <= 0) {
|
|
17
|
+
throw new RangeError('BoundedTailCapture capBytes must be a positive safe integer')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
push(chunk: Buffer): void {
|
|
22
|
+
if (chunk.length === 0) return
|
|
23
|
+
|
|
24
|
+
if (chunk.length >= this.capBytes) {
|
|
25
|
+
if (chunk.length > this.capBytes || this.length > 0) this.lostBytes = true
|
|
26
|
+
this.ensureCapacity(this.capBytes)
|
|
27
|
+
chunk.copy(this.storage, 0, chunk.length - this.capBytes)
|
|
28
|
+
this.start = 0
|
|
29
|
+
this.length = this.capBytes
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const combinedLength = this.length + chunk.length
|
|
34
|
+
if (combinedLength <= this.capBytes) {
|
|
35
|
+
this.ensureCapacity(combinedLength)
|
|
36
|
+
chunk.copy(this.storage, this.length)
|
|
37
|
+
this.length = combinedLength
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.lostBytes = true
|
|
42
|
+
this.ensureCapacity(this.capBytes)
|
|
43
|
+
const end = (this.start + this.length) % this.capBytes
|
|
44
|
+
const first = Math.min(chunk.length, this.capBytes - end)
|
|
45
|
+
chunk.copy(this.storage, end, 0, first)
|
|
46
|
+
if (first < chunk.length) chunk.copy(this.storage, 0, first)
|
|
47
|
+
this.start = (this.start + combinedLength - this.capBytes) % this.capBytes
|
|
48
|
+
this.length = this.capBytes
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Ordered copy of the retained raw bytes. */
|
|
52
|
+
get bytes(): Buffer {
|
|
53
|
+
const ordered = Buffer.allocUnsafe(this.length)
|
|
54
|
+
if (this.length === 0) return ordered
|
|
55
|
+
const first = Math.min(this.length, this.storage.length - this.start)
|
|
56
|
+
this.storage.copy(ordered, 0, this.start, this.start + first)
|
|
57
|
+
if (first < this.length) this.storage.copy(ordered, first, 0, this.length - first)
|
|
58
|
+
return ordered
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get text(): string {
|
|
62
|
+
const ordered = this.bytes
|
|
63
|
+
let offset = 0
|
|
64
|
+
// Tail eviction can leave continuation bytes from a valid UTF-8 code
|
|
65
|
+
// point at the new left edge. They are an artifact of the cut, not an
|
|
66
|
+
// authored replacement character, so discard only that partial prefix.
|
|
67
|
+
if (this.lostBytes) {
|
|
68
|
+
while (offset < ordered.length) {
|
|
69
|
+
const firstByte = ordered[offset]
|
|
70
|
+
if (firstByte === undefined || (firstByte & 0xc0) !== 0x80) break
|
|
71
|
+
offset++
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return ordered.toString('utf8', offset)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get truncated(): boolean {
|
|
78
|
+
return this.lostBytes
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Diagnostic invariant used by the internal killing observers. */
|
|
82
|
+
get retainedBytes(): number {
|
|
83
|
+
return this.length
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Diagnostic invariant used by the internal killing observers. */
|
|
87
|
+
get allocatedBytes(): number {
|
|
88
|
+
return this.storage.length
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private ensureCapacity(required: number): void {
|
|
92
|
+
if (this.storage.length >= required) return
|
|
93
|
+
let capacity = Math.max(1, this.storage.length)
|
|
94
|
+
while (capacity < required) capacity = Math.min(this.capBytes, capacity * 2)
|
|
95
|
+
const grown = Buffer.allocUnsafe(capacity)
|
|
96
|
+
if (this.length > 0) {
|
|
97
|
+
const first = Math.min(this.length, this.storage.length - this.start)
|
|
98
|
+
this.storage.copy(grown, 0, this.start, this.start + first)
|
|
99
|
+
if (first < this.length) this.storage.copy(grown, first, 0, this.length - first)
|
|
100
|
+
}
|
|
101
|
+
this.storage = grown
|
|
102
|
+
this.start = 0
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/execution/remote.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CommandExecutor,
|
|
2
3
|
CommandOptions,
|
|
3
4
|
CommandResult,
|
|
4
5
|
ExecutionCapability,
|
|
@@ -9,29 +10,41 @@ import type {
|
|
|
9
10
|
} from '../types/connector/index.js'
|
|
10
11
|
import type { Logger } from '../utils/logger.js'
|
|
11
12
|
import { BaseExecutionContext } from './base.js'
|
|
13
|
+
import { CommandCancellationUnsupportedError, RemoteExecutionBusyError } from './errors.js'
|
|
12
14
|
|
|
13
15
|
export interface RemoteExecutionContextOptions {
|
|
14
16
|
id: string
|
|
15
17
|
target: RemoteTarget
|
|
16
18
|
capabilities?: ExecutionCapability[]
|
|
19
|
+
/**
|
|
20
|
+
* Executes a command while preserving its argument boundaries at Namzu's
|
|
21
|
+
* remote seam. A downstream executor may still interpret `shell` according
|
|
22
|
+
* to its own target.
|
|
23
|
+
*/
|
|
24
|
+
commandExecutor?: CommandExecutor
|
|
25
|
+
/** @deprecated Use `commandExecutor`. */
|
|
17
26
|
commandHandler?: RemoteCommandHandler
|
|
18
27
|
log?: Logger
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
export class RemoteExecutionContext extends BaseExecutionContext {
|
|
30
|
+
export class RemoteExecutionContext extends BaseExecutionContext implements CommandExecutor {
|
|
22
31
|
readonly id: string
|
|
23
32
|
readonly environment: ExecutionEnvironment = 'remote'
|
|
24
33
|
|
|
25
34
|
private target: RemoteTarget
|
|
26
35
|
private connected = false
|
|
27
36
|
private capabilities: ExecutionCapability[]
|
|
37
|
+
private commandExecutor: CommandExecutor | undefined
|
|
28
38
|
private commandHandler: RemoteCommandHandler | undefined
|
|
39
|
+
private lifecycleAdmissionsOpen = true
|
|
40
|
+
private readonly activeCommands = new Set<symbol>()
|
|
29
41
|
|
|
30
42
|
constructor(options: RemoteExecutionContextOptions) {
|
|
31
43
|
super(options.log)
|
|
32
44
|
this.id = options.id
|
|
33
45
|
this.target = options.target
|
|
34
46
|
this.capabilities = options.capabilities ?? ['network']
|
|
47
|
+
this.commandExecutor = options.commandExecutor
|
|
35
48
|
this.commandHandler = options.commandHandler
|
|
36
49
|
}
|
|
37
50
|
|
|
@@ -43,18 +56,30 @@ export class RemoteExecutionContext extends BaseExecutionContext {
|
|
|
43
56
|
})
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
protected override onInitializationStarted(): void {
|
|
60
|
+
this.lifecycleAdmissionsOpen = false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected override onInitializationCommitted(): void {
|
|
64
|
+
this.lifecycleAdmissionsOpen = true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected override onTeardownRequested(): void {
|
|
68
|
+
this.lifecycleAdmissionsOpen = false
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
protected async doTeardown(): Promise<void> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.emit({
|
|
50
|
-
type: 'remote_disconnected',
|
|
51
|
-
contextId: this.id,
|
|
52
|
-
host: this.target.host,
|
|
53
|
-
})
|
|
54
|
-
}
|
|
72
|
+
this.assertIdleForDisconnect()
|
|
73
|
+
await this.disconnect()
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
async connect(): Promise<void> {
|
|
77
|
+
if (!this.lifecycleAdmissionsOpen) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Remote execution context "${this.id}" cannot connect until initialization commits after teardown or failed initialization.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (this.connected) return
|
|
58
83
|
this.connected = true
|
|
59
84
|
this.emit({
|
|
60
85
|
type: 'remote_connected',
|
|
@@ -70,6 +95,7 @@ export class RemoteExecutionContext extends BaseExecutionContext {
|
|
|
70
95
|
|
|
71
96
|
async disconnect(): Promise<void> {
|
|
72
97
|
if (!this.connected) return
|
|
98
|
+
this.assertIdleForDisconnect()
|
|
73
99
|
this.connected = false
|
|
74
100
|
this.emit({
|
|
75
101
|
type: 'remote_disconnected',
|
|
@@ -100,20 +126,49 @@ export class RemoteExecutionContext extends BaseExecutionContext {
|
|
|
100
126
|
return this.capabilities.includes(cap)
|
|
101
127
|
}
|
|
102
128
|
|
|
129
|
+
setCommandExecutor(executor: CommandExecutor): void {
|
|
130
|
+
this.commandExecutor = executor
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @deprecated Use `setCommandExecutor()`. */
|
|
103
134
|
setCommandHandler(handler: RemoteCommandHandler): void {
|
|
104
135
|
this.commandHandler = handler
|
|
105
136
|
}
|
|
106
137
|
|
|
107
|
-
async
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
async executeCommand(
|
|
139
|
+
command: string,
|
|
140
|
+
args: string[] = [],
|
|
141
|
+
options?: CommandOptions,
|
|
142
|
+
): Promise<CommandResult> {
|
|
143
|
+
if (this.commandExecutor) {
|
|
144
|
+
const executor = this.admitCommand(
|
|
145
|
+
this.commandExecutor,
|
|
146
|
+
`No remote command executor configured for context "${this.id}". Set one via setCommandExecutor() before calling executeCommand().`,
|
|
111
147
|
)
|
|
148
|
+
this.assertCancellationUnsupported(options?.signal)
|
|
149
|
+
return this.trackCommand(() => executor.executeCommand(command, args, options))
|
|
112
150
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
|
|
152
|
+
const handler = this.admitCommand(
|
|
153
|
+
this.commandHandler,
|
|
154
|
+
`No remote command executor configured for context "${this.id}". Set one via setCommandExecutor() before calling executeCommand().`,
|
|
155
|
+
)
|
|
156
|
+
this.assertCancellationUnsupported(options?.signal)
|
|
157
|
+
const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command
|
|
158
|
+
return this.trackCommand(() => handler.executeRemote(fullCommand, options))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @deprecated Use `executeCommand()` so command arguments retain their
|
|
163
|
+
* boundaries at Namzu's remote execution seam.
|
|
164
|
+
*/
|
|
165
|
+
async executeRemote(command: string, options?: CommandOptions): Promise<CommandResult> {
|
|
166
|
+
const handler = this.admitCommand(
|
|
167
|
+
this.commandHandler,
|
|
168
|
+
`No remote command handler configured for context "${this.id}". Set one via setCommandHandler() before calling executeRemote().`,
|
|
169
|
+
)
|
|
170
|
+
this.assertCancellationUnsupported(options?.signal)
|
|
171
|
+
return this.trackCommand(() => handler.executeRemote(command, options))
|
|
117
172
|
}
|
|
118
173
|
|
|
119
174
|
toConfig(): RemoteExecutionContextConfig {
|
|
@@ -133,4 +188,54 @@ export class RemoteExecutionContext extends BaseExecutionContext {
|
|
|
133
188
|
throw new Error(`Unsupported remote target type: "${target.type}"`)
|
|
134
189
|
}
|
|
135
190
|
}
|
|
191
|
+
|
|
192
|
+
private admitCommand<T>(implementation: T | undefined, missingMessage: string): T {
|
|
193
|
+
if (implementation === undefined) throw new Error(missingMessage)
|
|
194
|
+
if (!this.lifecycleAdmissionsOpen) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Remote execution context "${this.id}" is initializing, tearing down, or torn down. Wait for initialization to commit before executing another command.`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
if (!this.connected) {
|
|
200
|
+
throw new Error(`Remote context "${this.id}" is not connected. Call connect() first.`)
|
|
201
|
+
}
|
|
202
|
+
return implementation
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private assertCancellationUnsupported(signal: AbortSignal | undefined): void {
|
|
206
|
+
if (signal !== undefined) throw new CommandCancellationUnsupportedError(this.id)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private assertIdleForDisconnect(): void {
|
|
210
|
+
if (this.activeCommands.size > 0) {
|
|
211
|
+
throw new RemoteExecutionBusyError(this.id, this.activeCommands.size)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private trackCommand(invoke: () => Promise<CommandResult>): Promise<CommandResult> {
|
|
216
|
+
const ownership = Symbol('remote-command')
|
|
217
|
+
this.activeCommands.add(ownership)
|
|
218
|
+
const release = (): void => {
|
|
219
|
+
this.activeCommands.delete(ownership)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let operation: Promise<CommandResult>
|
|
223
|
+
try {
|
|
224
|
+
operation = Promise.resolve(invoke())
|
|
225
|
+
} catch (error) {
|
|
226
|
+
release()
|
|
227
|
+
return Promise.reject(error)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return operation.then(
|
|
231
|
+
(result) => {
|
|
232
|
+
release()
|
|
233
|
+
return result
|
|
234
|
+
},
|
|
235
|
+
(error: unknown) => {
|
|
236
|
+
release()
|
|
237
|
+
throw error
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
}
|
|
136
241
|
}
|
package/src/plugin/lifecycle.ts
CHANGED
|
@@ -711,6 +711,12 @@ export class PluginLifecycleManager {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
const results: PluginHookResult[] = []
|
|
714
|
+
// Interrupt hooks are cleanup/notification observers. One extension's
|
|
715
|
+
// skip, error, retry, or timeout must not suppress the extensions that
|
|
716
|
+
// follow it, and none of those results can change the cancellation that
|
|
717
|
+
// already happened. Other hook events retain their existing flow-control
|
|
718
|
+
// semantics.
|
|
719
|
+
const observationalFanOut = event === 'run_interrupt'
|
|
714
720
|
|
|
715
721
|
// Determine execution order: post_* hooks run backward (for cleanup semantics)
|
|
716
722
|
const isPost = event.startsWith('post_')
|
|
@@ -830,16 +836,16 @@ export class PluginLifecycleManager {
|
|
|
830
836
|
|
|
831
837
|
results.push(result)
|
|
832
838
|
|
|
833
|
-
if (result.action === 'modify') {
|
|
839
|
+
if (!observationalFanOut && result.action === 'modify') {
|
|
834
840
|
toolInputOverlay = result.input
|
|
835
841
|
}
|
|
836
842
|
|
|
837
843
|
// Handle flow control: check priority order: error > skip > retry > resume > modify > continue
|
|
838
844
|
// Short-circuit on error or skip; return immediately on resume or retry
|
|
839
|
-
if (result.action === 'error' || result.action === 'skip') {
|
|
845
|
+
if (!observationalFanOut && (result.action === 'error' || result.action === 'skip')) {
|
|
840
846
|
break
|
|
841
847
|
}
|
|
842
|
-
if (result.action === 'retry') {
|
|
848
|
+
if (!observationalFanOut && result.action === 'retry') {
|
|
843
849
|
break
|
|
844
850
|
}
|
|
845
851
|
}
|
package/src/process/kill-tree.ts
CHANGED
|
@@ -10,23 +10,21 @@ import type { spawn } from 'node:child_process'
|
|
|
10
10
|
* paid for by a bug where a cancelled command kept running, and a
|
|
11
11
|
* near-copy would reproduce the bug in the copy.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* and leaves `cmd`, and anything it forked, running past both a cancel and a
|
|
20
|
-
* timeout.
|
|
13
|
+
* Some callers reach a command through a shell or isolation wrapper; the
|
|
14
|
+
* local execution context can also spawn the authored executable directly.
|
|
15
|
+
* In every case, signalling only `child.pid` — which is both what
|
|
16
|
+
* `child.kill()` does and what `spawn`'s own `signal`/`timeout` options do —
|
|
17
|
+
* can reap that direct child while leaving a descendant running past both a
|
|
18
|
+
* cancel and a timeout.
|
|
21
19
|
*
|
|
22
20
|
* POSIX: the caller must spawn with `detached: true`, which makes the child
|
|
23
21
|
* the leader of a new process group (pgid === pid) instead of joining this
|
|
24
22
|
* Node process's own. A negative pid signals that whole group in one call.
|
|
25
|
-
* Group membership is a host-kernel fact that
|
|
26
|
-
* from its parent
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* Group membership is a host-kernel fact that an ordinary forked descendant
|
|
24
|
+
* inherits from its parent, including under the namespaced isolation tiers.
|
|
25
|
+
* A process which deliberately starts a new session/process group can escape
|
|
26
|
+
* this boundary; this helper is process-group signalling, not a portable
|
|
27
|
+
* arbitrary-descendant ownership primitive.
|
|
30
28
|
*
|
|
31
29
|
* Windows has no process-group id to sign a kill with — `process.kill` with
|
|
32
30
|
* a negative pid there either throws or silently does nothing, depending on
|
|
@@ -10,6 +10,8 @@ export interface ResolvedProviderCapabilities {
|
|
|
10
10
|
supportsFunctionCalling: boolean
|
|
11
11
|
supportsVision: boolean
|
|
12
12
|
supportsDocuments: boolean
|
|
13
|
+
supportsToolResultImages: boolean
|
|
14
|
+
supportsToolResultDocuments: boolean
|
|
13
15
|
maxOutputTokens?: number
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -25,6 +27,8 @@ export const PERMISSIVE_PROVIDER_CAPABILITIES: ResolvedProviderCapabilities = {
|
|
|
25
27
|
supportsFunctionCalling: true,
|
|
26
28
|
supportsVision: true,
|
|
27
29
|
supportsDocuments: true,
|
|
30
|
+
supportsToolResultImages: true,
|
|
31
|
+
supportsToolResultDocuments: true,
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/**
|
|
@@ -46,6 +50,12 @@ export function resolveProviderCapabilities(
|
|
|
46
50
|
supportsVision: declared?.supportsVision ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsVision,
|
|
47
51
|
supportsDocuments:
|
|
48
52
|
declared?.supportsDocuments ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsDocuments,
|
|
53
|
+
supportsToolResultImages:
|
|
54
|
+
declared?.supportsToolResultImages ??
|
|
55
|
+
PERMISSIVE_PROVIDER_CAPABILITIES.supportsToolResultImages,
|
|
56
|
+
supportsToolResultDocuments:
|
|
57
|
+
declared?.supportsToolResultDocuments ??
|
|
58
|
+
PERMISSIVE_PROVIDER_CAPABILITIES.supportsToolResultDocuments,
|
|
49
59
|
maxOutputTokens: declared?.maxOutputTokens,
|
|
50
60
|
}
|
|
51
61
|
}
|