@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.
Files changed (260) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/connector/index.d.ts +1 -1
  5. package/dist/connector/index.d.ts.map +1 -1
  6. package/dist/connector/index.js +1 -1
  7. package/dist/connector/index.js.map +1 -1
  8. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  9. package/dist/connector/mcp/adapter.js +71 -16
  10. package/dist/connector/mcp/adapter.js.map +1 -1
  11. package/dist/connector/mcp/client.d.ts.map +1 -1
  12. package/dist/connector/mcp/client.js +3 -2
  13. package/dist/connector/mcp/client.js.map +1 -1
  14. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  15. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  16. package/dist/connector/mcp/http-redirect.js +31 -0
  17. package/dist/connector/mcp/http-redirect.js.map +1 -0
  18. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  19. package/dist/connector/mcp/http-sse.js +5 -0
  20. package/dist/connector/mcp/http-sse.js.map +1 -1
  21. package/dist/connector/mcp/image-admission.d.ts +16 -0
  22. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  23. package/dist/connector/mcp/image-admission.js +288 -0
  24. package/dist/connector/mcp/image-admission.js.map +1 -0
  25. package/dist/connector/mcp/stdio.d.ts +13 -0
  26. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  27. package/dist/connector/mcp/stdio.js +66 -23
  28. package/dist/connector/mcp/stdio.js.map +1 -1
  29. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  30. package/dist/connector/mcp/streamable-http.js +3 -0
  31. package/dist/connector/mcp/streamable-http.js.map +1 -1
  32. package/dist/constants/tools/index.js +1 -1
  33. package/dist/constants/tools/index.js.map +1 -1
  34. package/dist/execution/base.d.ts +13 -0
  35. package/dist/execution/base.d.ts.map +1 -1
  36. package/dist/execution/base.js +103 -8
  37. package/dist/execution/base.js.map +1 -1
  38. package/dist/execution/errors.d.ts +12 -0
  39. package/dist/execution/errors.d.ts.map +1 -0
  40. package/dist/execution/errors.js +21 -0
  41. package/dist/execution/errors.js.map +1 -0
  42. package/dist/execution/factory.d.ts.map +1 -1
  43. package/dist/execution/factory.js +1 -0
  44. package/dist/execution/factory.js.map +1 -1
  45. package/dist/execution/hybrid.d.ts +5 -0
  46. package/dist/execution/hybrid.d.ts.map +1 -1
  47. package/dist/execution/hybrid.js +36 -16
  48. package/dist/execution/hybrid.js.map +1 -1
  49. package/dist/execution/index.d.ts +1 -0
  50. package/dist/execution/index.d.ts.map +1 -1
  51. package/dist/execution/index.js +1 -0
  52. package/dist/execution/index.js.map +1 -1
  53. package/dist/execution/local.d.ts +13 -0
  54. package/dist/execution/local.d.ts.map +1 -1
  55. package/dist/execution/local.js +145 -19
  56. package/dist/execution/local.js.map +1 -1
  57. package/dist/execution/output.d.ts +27 -0
  58. package/dist/execution/output.d.ts.map +1 -0
  59. package/dist/execution/output.js +105 -0
  60. package/dist/execution/output.js.map +1 -0
  61. package/dist/execution/remote.d.ts +26 -2
  62. package/dist/execution/remote.d.ts.map +1 -1
  63. package/dist/execution/remote.js +86 -15
  64. package/dist/execution/remote.js.map +1 -1
  65. package/dist/plugin/lifecycle.d.ts.map +1 -1
  66. package/dist/plugin/lifecycle.js +9 -3
  67. package/dist/plugin/lifecycle.js.map +1 -1
  68. package/dist/process/kill-tree.d.ts +11 -13
  69. package/dist/process/kill-tree.d.ts.map +1 -1
  70. package/dist/process/kill-tree.js +11 -13
  71. package/dist/process/kill-tree.js.map +1 -1
  72. package/dist/provider/fallback.d.ts.map +1 -1
  73. package/dist/provider/fallback.js +31 -0
  74. package/dist/provider/fallback.js.map +1 -1
  75. package/dist/provider/idle-timeout.d.ts.map +1 -1
  76. package/dist/provider/idle-timeout.js +5 -0
  77. package/dist/provider/idle-timeout.js.map +1 -1
  78. package/dist/provider/retry.d.ts.map +1 -1
  79. package/dist/provider/retry.js +5 -0
  80. package/dist/provider/retry.js.map +1 -1
  81. package/dist/public-runtime.d.ts +2 -2
  82. package/dist/public-runtime.d.ts.map +1 -1
  83. package/dist/public-runtime.js +2 -2
  84. package/dist/public-runtime.js.map +1 -1
  85. package/dist/registry/tool/execute.d.ts +7 -1
  86. package/dist/registry/tool/execute.d.ts.map +1 -1
  87. package/dist/registry/tool/execute.js +201 -54
  88. package/dist/registry/tool/execute.js.map +1 -1
  89. package/dist/run/command-gate.d.ts.map +1 -1
  90. package/dist/run/command-gate.js +11 -0
  91. package/dist/run/command-gate.js.map +1 -1
  92. package/dist/run/workspace-fingerprint.d.ts +4 -4
  93. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  94. package/dist/run/workspace-fingerprint.js +6 -4
  95. package/dist/run/workspace-fingerprint.js.map +1 -1
  96. package/dist/runtime/bidi/index.d.ts +1 -1
  97. package/dist/runtime/bidi/index.d.ts.map +1 -1
  98. package/dist/runtime/bidi/index.js +1 -1
  99. package/dist/runtime/bidi/index.js.map +1 -1
  100. package/dist/runtime/bidi/session.d.ts +19 -0
  101. package/dist/runtime/bidi/session.d.ts.map +1 -1
  102. package/dist/runtime/bidi/session.js +150 -26
  103. package/dist/runtime/bidi/session.js.map +1 -1
  104. package/dist/runtime/query/executor.d.ts +43 -1
  105. package/dist/runtime/query/executor.d.ts.map +1 -1
  106. package/dist/runtime/query/executor.js +626 -161
  107. package/dist/runtime/query/executor.js.map +1 -1
  108. package/dist/runtime/query/index.d.ts.map +1 -1
  109. package/dist/runtime/query/index.js +46 -4
  110. package/dist/runtime/query/index.js.map +1 -1
  111. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  112. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  113. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  114. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  115. package/dist/runtime/query/request-rich-content.js +33 -13
  116. package/dist/runtime/query/request-rich-content.js.map +1 -1
  117. package/dist/runtime/query/resume-pending.d.ts +5 -1
  118. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  119. package/dist/runtime/query/resume-pending.js +66 -1
  120. package/dist/runtime/query/resume-pending.js.map +1 -1
  121. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  122. package/dist/runtime/query/tool-output-budget.js +45 -16
  123. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  124. package/dist/runtime/query/tooling.d.ts +6 -0
  125. package/dist/runtime/query/tooling.d.ts.map +1 -1
  126. package/dist/runtime/query/tooling.js +4 -0
  127. package/dist/runtime/query/tooling.js.map +1 -1
  128. package/dist/sandbox/provider/local.d.ts.map +1 -1
  129. package/dist/sandbox/provider/local.js +193 -41
  130. package/dist/sandbox/provider/local.js.map +1 -1
  131. package/dist/scheduler/delegating.d.ts +2 -1
  132. package/dist/scheduler/delegating.d.ts.map +1 -1
  133. package/dist/scheduler/delegating.js +6 -5
  134. package/dist/scheduler/delegating.js.map +1 -1
  135. package/dist/scheduler/local.d.ts +2 -1
  136. package/dist/scheduler/local.d.ts.map +1 -1
  137. package/dist/scheduler/local.js +2 -2
  138. package/dist/scheduler/local.js.map +1 -1
  139. package/dist/session/workspace/driver.d.ts +12 -4
  140. package/dist/session/workspace/driver.d.ts.map +1 -1
  141. package/dist/session/workspace/git-worktree.d.ts +17 -3
  142. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  143. package/dist/session/workspace/git-worktree.js +153 -34
  144. package/dist/session/workspace/git-worktree.js.map +1 -1
  145. package/dist/skills/registry.d.ts +15 -0
  146. package/dist/skills/registry.d.ts.map +1 -1
  147. package/dist/skills/registry.js +33 -0
  148. package/dist/skills/registry.js.map +1 -1
  149. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  150. package/dist/tools/builtins/computer-use.js +39 -2
  151. package/dist/tools/builtins/computer-use.js.map +1 -1
  152. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  153. package/dist/tools/builtins/run-code.js +9 -11
  154. package/dist/tools/builtins/run-code.js.map +1 -1
  155. package/dist/tools/builtins/skill.d.ts +2 -1
  156. package/dist/tools/builtins/skill.d.ts.map +1 -1
  157. package/dist/tools/builtins/skill.js +250 -6
  158. package/dist/tools/builtins/skill.js.map +1 -1
  159. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  160. package/dist/tools/coordinator/agent.js +100 -30
  161. package/dist/tools/coordinator/agent.js.map +1 -1
  162. package/dist/types/agent/manager.d.ts +1 -1
  163. package/dist/types/agent/manager.d.ts.map +1 -1
  164. package/dist/types/agent/scheduler.d.ts +10 -1
  165. package/dist/types/agent/scheduler.d.ts.map +1 -1
  166. package/dist/types/authorization/index.d.ts +12 -12
  167. package/dist/types/bidi/index.d.ts +19 -2
  168. package/dist/types/bidi/index.d.ts.map +1 -1
  169. package/dist/types/computer-use/index.d.ts +18 -0
  170. package/dist/types/computer-use/index.d.ts.map +1 -1
  171. package/dist/types/connector/execution-context.d.ts +3 -1
  172. package/dist/types/connector/execution-context.d.ts.map +1 -1
  173. package/dist/types/execution/index.d.ts +43 -1
  174. package/dist/types/execution/index.d.ts.map +1 -1
  175. package/dist/types/hitl/index.d.ts +11 -0
  176. package/dist/types/hitl/index.d.ts.map +1 -1
  177. package/dist/types/hitl/index.js.map +1 -1
  178. package/dist/types/message/index.d.ts +1 -1
  179. package/dist/types/message/index.d.ts.map +1 -1
  180. package/dist/types/message/index.js +2 -1
  181. package/dist/types/message/index.js.map +1 -1
  182. package/dist/types/plugin/index.d.ts +14 -1
  183. package/dist/types/plugin/index.d.ts.map +1 -1
  184. package/dist/types/plugin/index.js +1 -0
  185. package/dist/types/plugin/index.js.map +1 -1
  186. package/dist/types/provider/interface.d.ts +14 -0
  187. package/dist/types/provider/interface.d.ts.map +1 -1
  188. package/dist/types/sandbox/index.d.ts +15 -10
  189. package/dist/types/sandbox/index.d.ts.map +1 -1
  190. package/dist/types/sandbox/index.js.map +1 -1
  191. package/dist/types/tool/index.d.ts +77 -6
  192. package/dist/types/tool/index.d.ts.map +1 -1
  193. package/dist/types/tool/index.js.map +1 -1
  194. package/dist/types/workspace/ref.d.ts +5 -0
  195. package/dist/types/workspace/ref.d.ts.map +1 -1
  196. package/dist/utils/abort.d.ts +8 -0
  197. package/dist/utils/abort.d.ts.map +1 -1
  198. package/dist/utils/abort.js +16 -0
  199. package/dist/utils/abort.js.map +1 -1
  200. package/package.json +4 -4
  201. package/src/authorization/gate.ts +1 -1
  202. package/src/connector/index.ts +2 -0
  203. package/src/connector/mcp/adapter.ts +82 -21
  204. package/src/connector/mcp/client.ts +2 -1
  205. package/src/connector/mcp/http-redirect.ts +32 -0
  206. package/src/connector/mcp/http-sse.ts +5 -0
  207. package/src/connector/mcp/image-admission.ts +272 -0
  208. package/src/connector/mcp/stdio.ts +63 -21
  209. package/src/connector/mcp/streamable-http.ts +3 -0
  210. package/src/constants/tools/index.ts +1 -1
  211. package/src/execution/base.ts +124 -7
  212. package/src/execution/errors.ts +24 -0
  213. package/src/execution/factory.ts +1 -0
  214. package/src/execution/hybrid.ts +47 -16
  215. package/src/execution/index.ts +1 -0
  216. package/src/execution/local.ts +164 -19
  217. package/src/execution/output.ts +104 -0
  218. package/src/execution/remote.ts +122 -17
  219. package/src/plugin/lifecycle.ts +9 -3
  220. package/src/process/kill-tree.ts +11 -13
  221. package/src/provider/fallback.ts +39 -0
  222. package/src/provider/idle-timeout.ts +8 -0
  223. package/src/provider/retry.ts +8 -0
  224. package/src/public-runtime.ts +7 -1
  225. package/src/registry/tool/execute.ts +230 -64
  226. package/src/run/command-gate.ts +12 -0
  227. package/src/run/workspace-fingerprint.ts +5 -4
  228. package/src/runtime/bidi/index.ts +1 -1
  229. package/src/runtime/bidi/session.ts +177 -28
  230. package/src/runtime/query/executor.ts +769 -163
  231. package/src/runtime/query/index.ts +53 -6
  232. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  233. package/src/runtime/query/request-rich-content.ts +39 -14
  234. package/src/runtime/query/resume-pending.ts +83 -1
  235. package/src/runtime/query/tool-output-budget.ts +52 -17
  236. package/src/runtime/query/tooling.ts +10 -0
  237. package/src/sandbox/provider/local.ts +213 -45
  238. package/src/scheduler/delegating.ts +6 -5
  239. package/src/scheduler/local.ts +3 -2
  240. package/src/session/workspace/driver.ts +12 -4
  241. package/src/session/workspace/git-worktree.ts +173 -34
  242. package/src/skills/registry.ts +47 -0
  243. package/src/tools/builtins/computer-use.ts +45 -2
  244. package/src/tools/builtins/run-code.ts +9 -11
  245. package/src/tools/builtins/skill.ts +325 -7
  246. package/src/tools/coordinator/agent.ts +108 -33
  247. package/src/types/agent/manager.ts +1 -1
  248. package/src/types/agent/scheduler.ts +10 -1
  249. package/src/types/bidi/index.ts +19 -2
  250. package/src/types/computer-use/index.ts +19 -0
  251. package/src/types/connector/execution-context.ts +3 -0
  252. package/src/types/execution/index.ts +46 -1
  253. package/src/types/hitl/index.ts +11 -0
  254. package/src/types/message/index.ts +3 -2
  255. package/src/types/plugin/index.ts +15 -0
  256. package/src/types/provider/interface.ts +18 -0
  257. package/src/types/sandbox/index.ts +15 -10
  258. package/src/types/tool/index.ts +85 -6
  259. package/src/types/workspace/ref.ts +5 -0
  260. package/src/utils/abort.ts +18 -0
@@ -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 teardownPromises: Promise<void>[] = []
88
- for (const remote of this.remoteCtxs.values()) {
89
- teardownPromises.push(remote.teardown())
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 promises: Promise<void>[] = []
126
- for (const remote of this.remoteCtxs.values()) {
127
- promises.push(remote.disconnect())
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
- const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command
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
- const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command
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: this.localCtx.getCwd(),
182
- fsAccess: this.localCtx.hasFsAccess(),
183
- envVars: this.localCtx.getEnvVars(),
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,
@@ -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'
@@ -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 async doTeardown(): Promise<void> {}
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 ?? 30_000
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
- return new Promise<CommandResult>((resolvePromise) => {
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
- timeout: timeoutMs,
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
- let stdout = ''
124
- let stderr = ''
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 += data.toString()
239
+ stdout.push(data)
128
240
  })
129
241
 
130
242
  proc.stderr?.on('data', (data: Buffer) => {
131
- stderr += data.toString()
243
+ stderr.push(data)
132
244
  })
133
245
 
134
- proc.on('close', (code) => {
135
- resolvePromise({
136
- exitCode: code ?? 1,
137
- stdout,
138
- stderr,
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.on('error', (err) => {
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: err.message,
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
+ }