@namzu/sdk 32.0.0 → 33.0.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 +127 -0
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +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/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.map +1 -1
- package/dist/runtime/query/index.js +46 -4
- package/dist/runtime/query/index.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/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 +39 -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/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/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.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/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/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/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 +53 -6
- 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/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 +45 -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/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/interface.ts +18 -0
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
package/src/execution/hybrid.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface HybridExecutionContextOptions {
|
|
|
21
21
|
envVars?: Record<string, string>
|
|
22
22
|
capabilities?: ExecutionCapability[]
|
|
23
23
|
shell?: string
|
|
24
|
+
maxOutputBytes?: number
|
|
24
25
|
}
|
|
25
26
|
remotes: RemoteTarget[]
|
|
26
27
|
routingStrategy?: ExecutionRoutingStrategy
|
|
@@ -36,6 +37,7 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
36
37
|
private remoteTargets: RemoteTarget[]
|
|
37
38
|
private routingStrategy: ExecutionRoutingStrategy
|
|
38
39
|
private roundRobinIndex = 0
|
|
40
|
+
private admissionsOpen = true
|
|
39
41
|
|
|
40
42
|
constructor(options: HybridExecutionContextOptions) {
|
|
41
43
|
super(options.log)
|
|
@@ -50,6 +52,7 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
50
52
|
envVars: options.local.envVars,
|
|
51
53
|
capabilities: options.local.capabilities,
|
|
52
54
|
shell: options.local.shell,
|
|
55
|
+
maxOutputBytes: options.local.maxOutputBytes,
|
|
53
56
|
log: options.log,
|
|
54
57
|
})
|
|
55
58
|
|
|
@@ -83,13 +86,29 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
83
86
|
})
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
protected override onInitializationStarted(): void {
|
|
90
|
+
this.admissionsOpen = false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected override onInitializationCommitted(): void {
|
|
94
|
+
this.admissionsOpen = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected override onTeardownRequested(): void {
|
|
98
|
+
this.admissionsOpen = false
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
protected async doTeardown(): Promise<void> {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
const contexts: BaseExecutionContext[] = [this.localCtx, ...this.remoteCtxs.values()]
|
|
103
|
+
const results = await Promise.allSettled(contexts.map((context) => context.teardown()))
|
|
104
|
+
const failures = results.flatMap((result) =>
|
|
105
|
+
result.status === 'rejected' ? [result.reason] : [],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (failures.length === 1) throw failures[0]
|
|
109
|
+
if (failures.length > 1) {
|
|
110
|
+
throw new AggregateError(failures, 'Multiple hybrid execution contexts failed to tear down')
|
|
90
111
|
}
|
|
91
|
-
await Promise.allSettled(teardownPromises)
|
|
92
|
-
await this.localCtx.teardown()
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
getLocal(): LocalExecutionContext {
|
|
@@ -122,11 +141,16 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
async disconnectAllRemotes(): Promise<void> {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
const results = await Promise.allSettled(
|
|
145
|
+
Array.from(this.remoteCtxs.values(), (remote) => remote.disconnect()),
|
|
146
|
+
)
|
|
147
|
+
const failures = results.flatMap((result) =>
|
|
148
|
+
result.status === 'rejected' ? [result.reason] : [],
|
|
149
|
+
)
|
|
150
|
+
if (failures.length === 1) throw failures[0]
|
|
151
|
+
if (failures.length > 1) {
|
|
152
|
+
throw new AggregateError(failures, 'Multiple remote execution contexts failed to disconnect')
|
|
128
153
|
}
|
|
129
|
-
await Promise.allSettled(promises)
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
async executeCommand(
|
|
@@ -134,6 +158,11 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
134
158
|
args: string[] = [],
|
|
135
159
|
options?: CommandOptions,
|
|
136
160
|
): Promise<CommandResult> {
|
|
161
|
+
if (!this.admissionsOpen) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Hybrid execution context "${this.id}" is initializing, tearing down, or torn down. Wait for initialization to commit before executing another command.`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
137
166
|
switch (this.routingStrategy) {
|
|
138
167
|
case 'local-first':
|
|
139
168
|
return this.localCtx.executeCommand(command, args, options)
|
|
@@ -141,8 +170,7 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
141
170
|
case 'remote-first': {
|
|
142
171
|
const connectedRemote = this.getFirstConnectedRemote()
|
|
143
172
|
if (connectedRemote) {
|
|
144
|
-
|
|
145
|
-
return connectedRemote.executeRemote(fullCommand, options)
|
|
173
|
+
return connectedRemote.executeCommand(command, args, options)
|
|
146
174
|
}
|
|
147
175
|
return this.localCtx.executeCommand(command, args, options)
|
|
148
176
|
}
|
|
@@ -162,8 +190,7 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
162
190
|
if (target === this.localCtx) {
|
|
163
191
|
return this.localCtx.executeCommand(command, args, options)
|
|
164
192
|
}
|
|
165
|
-
|
|
166
|
-
return (target as RemoteExecutionContext).executeRemote(fullCommand, options)
|
|
193
|
+
return (target as RemoteExecutionContext).executeCommand(command, args, options)
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
default: {
|
|
@@ -174,13 +201,17 @@ export class HybridExecutionContext extends BaseExecutionContext implements Comm
|
|
|
174
201
|
}
|
|
175
202
|
|
|
176
203
|
toConfig(): HybridExecutionContextConfig {
|
|
204
|
+
const local = this.localCtx.toConfig()
|
|
177
205
|
return {
|
|
178
206
|
id: this.id,
|
|
179
207
|
environment: 'hybrid',
|
|
180
208
|
local: {
|
|
181
|
-
cwd:
|
|
182
|
-
fsAccess:
|
|
183
|
-
envVars:
|
|
209
|
+
cwd: local.cwd,
|
|
210
|
+
fsAccess: local.fsAccess,
|
|
211
|
+
envVars: local.envVars,
|
|
212
|
+
capabilities: local.capabilities,
|
|
213
|
+
shell: local.shell,
|
|
214
|
+
maxOutputBytes: local.maxOutputBytes,
|
|
184
215
|
},
|
|
185
216
|
remotes: this.remoteTargets.map((t) => ({ ...t })),
|
|
186
217
|
routingStrategy: this.routingStrategy,
|
package/src/execution/index.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
export { BaseExecutionContext } from './base.js'
|
|
21
|
+
export { CommandCancellationUnsupportedError, RemoteExecutionBusyError } from './errors.js'
|
|
21
22
|
export { ExecutionContextFactory } from './factory.js'
|
|
22
23
|
export { HybridExecutionContext } from './hybrid.js'
|
|
23
24
|
export type { HybridExecutionContextOptions } from './hybrid.js'
|
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
|
+
}
|